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 = "";
}
}