Home > Enterprise >  Extend an interface with new set of properties based on original interface with mapped types
Extend an interface with new set of properties based on original interface with mapped types

Time:10-18

Supposed I have the following interface:

interface Person {
    name: string;
    age: number;
    location: string;
}

I want to extend it to have 3 additional properties, one for each of the interfaces' original properties.

So final outcome I want is:

interface Person {
    name: string;
    nameHidden: boolean;
    age: number;
    ageHidden: boolean;
    location: string;
    locationHidden: boolean;
}

I was looking into mapped types by TypeScript: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

But I am unable to figure out how I can achieve this behavior. It only shows examples of how to re-map the existing properties, but not add new ones on top of the existing ones.

CodePudding user response:

You've said you don't need it to be the same interface. If so, you can do this:

type WithFlags<T> = T & {
    [Key in keyof T as Key extends string ? `${Key}Hidden` : never]: boolean;
};

type PersonWithFlags = WithFlags<Person>;

Playground example

That creates a new type that is the passed-in type T plus a property for every property in it (at least the ones whose names extend string) with Hidden added to the name and the type boolean.

Titian Cernicova-Dragomir points out in a comment (thanks!) that we can avoid the conditional in that key mapping by using & string instead, which is a bit shorter:

type WithFlags<T> = T & {
    [Key in keyof T & string as `${Key}Hidden`]: boolean;
// −−−−−−−−−−−−−−−−^^^^^^^^^
};

Playground link

pilchard points out in a comment that the documentation has virtually this same example. They do the remapping like Titian, but in a different place:

type WithFlags<T> = T & {
    [Key in keyof T as `${Key & string}Hidden`]: boolean;
// −−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^
};

Playground link

CodePudding user response:

I took T.J. Crowder's answer and modified it a little to make the type a little more (re)usable:

type WithFlags<T, TFlag extends string, TValue = null> = T & {
    [Key in keyof T & string as `${Key}${TFlag}`]: TValue extends null ? T[Key] : TValue;
};

By default, you have to pass the interface and the flag name, which results in the following type based on the Person interface and TFlag = Hidden:

{
  name: string;
  age: number;
  location: string;
  nameHidden: string;
  ageHidden: number;
  locationHidden: string;
}

If you want the additional properties to be of a specific type like boolean you can adjust the 3rd type parameter TValue.

To fulfill your requirements, you can have a helper type like:

type WithHiddenFlags<T> = WithFlags<T, 'Hidden', boolean>;

and for WithHiddenFlags<Person> the resulting type is:

{
  name: string;
  age: number;
  location: string;
  nameHidden: boolean;
  ageHidden: boolean;
  locationHidden: boolean;
}

The only limitation that I see is that for TValue = null, the resulting properties will have the original properties' type instead of null.

  • Related