Home > Software design >  Implement interface in a way that when a class implements it, it **must** implement all properties o
Implement interface in a way that when a class implements it, it **must** implement all properties o

Time:11-10

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);
}

See https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585

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  
}

Playground

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

Playground link

  • Related