I want to implement interface in a way that when a class implements it, it must implement all properties of the interface either as optional or not optional.
If I have
interface Person {
name: string,
address?: string,
}
class Fisherman implements Person {
name: string;
fishingRodColor: string;
// and any more properties I want
}
Typescript won't raise an error, it's okay Fisherman doesn't implement address because it's optional, but I do want it to raise an error, I want it to force me to write Fisherman class as one of those two choices:
class Fisherman implements Person {
name: string;
address?: string;
fishingRodColor: string;
// and any more properties I want
}
or
class Fisherman implements Person {
name: string;
address: string;
fishingRodColor: string;
// and any more properties I want
}
both should be allowed, and at least one of those implementation should be required.
If I simply write my Person interface as such with address not being optional
interface Person {
name: string,
address: string,
}
It will not allow me to write this class which I want it to allow me:
class Fisherman implements Person {
name: string;
address?: string;
fishingRodColor: string;
// and any more properties I want
}
Because it will say address can't be optional since it isn't optional in Person interface.
Edit: I appreciate the help, but another need I have with the solution is that for my use-case, In the case where address is optional, I would like to be able to declare this const object:
const fisherman: Fisherman: {
name: 'George'
fishingRodColor: 'Red'
}
But if address: string | undefined, typescript will still raise an error saying fisherman must have the property address. Typescript wants me to do this:
const fisherman: Fisherman: {
name: 'George'
fishingRodColor: 'Red'
address: undefined
}
But I don't want (and I think I also can't in new Mongo versions since undefined was deprecated) to push a field with undefined in it into my database.
I can do workarounds like deleting undefineds before inserting into DB, but it probably makes using the interface not worth it for this use-case.
CodePudding user response:
You could transform all optional properties with type T to T | undefined with:
type Complete<T> = {
[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : (T[P] | undefined);
}
In your example:
class FishermanError implements Complete<Person> {
name: string;
fishingRodColor: string;
// error: Property 'address' is missing
}
class Fisherman implements Complete<Person> {
name: string;
fishingRodColor: string;
address: string
// ok
}
class Fisherman2 implements Complete<Person> {
name: string;
fishingRodColor: string;
address: string | undefined
// ok
}
CodePudding user response:
I don't think you can do exactly what you've said you want to do, but you can get close: You can require that address be declared, but if the original is address?: string, you'll end up having to declare it as address: string | undefined or address: string (not address?: string). That's not quite the same thing as the property not being there although they're mostly (but not entirely) treated the same way. (That said note that TypeScript recently got a exactOptionalPropertyTypes flag related to this.)
(Edit: Lesiak has a much more concise version of the below with clever use of Pick and Required from this article.)
The way I can see to get there uses two utility types and a mapped type with remapped modifiers. The utility types (I got OptionalKeys from this answer by jcalz, then modified it to get RequiredKeys):
type OptionalKeys<T> = T extends any
? {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T]
: never;
type RequiredKeys<T> = T extends any
? {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T]
: never;
Those let us figure out which keys are optional and which are required. Then we can use a mapped type (well, a pair of them combined) to change addres?: string to address: string | undefined:
type NoOptional<T extends object> =
{
[key in RequiredKeys<T>]: T[key];
} &
{
[key in OptionalKeys<T>]-?: T[key] | undefined;
// Removes the optionality −−−−^^ ^^^^^^^^^^^^−−− adds `undefined` to
// the property type
};
Then we get an error here as desired:
class BadFisherman implements NoOptional<Person> { // Error as desired
name: string;
fishingRodColor: string;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
}
}
but this works:
class GoodFisherman1 implements NoOptional<Person> {
name: string;
fishingRodColor: string;
address: string | undefined;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
}
}
and this works:
class GoodFisherman2 implements NoOptional<Person> {
name: string;
fishingRodColor: string;
address: string;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
this.address = "";
}
}
