Home > Net >  Array of enum to record type
Array of enum to record type

Time:05-04

Say I have a function that takes a parameter of an array of enum values:

enum HatType {
  Big,
  Flat,
}

const fn = (types: HatType[]) => {
  ...
};

fn([ HatTypes.Big ]);

and within fn, I return properties on the return type based upon what enum values are passed:

const hatResults = fn([ HatTypes.Big ]);
// Returns `{ bigHats: BigHat[] }`
const moreHatResults = fn([ HatTypes.Flat ]);
// Returns `{ flatHats: FlatHat[] }`
const allTheHatResults = fn([ HatTypes.Big, HatTypes.Flat ]);
// Returns `{ bigHats: BigHat[], flatHats: FlatHat[] }`

Is it possible to type the return value based upon the enum values I've passed in? That is, { bigHats: BigHat[] } or { flatHats: FlatHat[] } or { bigHats: BigHat[], flatHats: FlatHat[] } for the above

I'm thinking that I could possibly by having a union type from the enum array and using Pick<AllHats, pickedHatsUnion> to do this. But this requires the as const assignment to the array to generate the union correctly, which I couldn't find a way to do with a function parameter.

Is this possible?


AllHats is an exemplar type, but would be something like:

interface AllHats {
  bigHats: BigHat[],
  flatHats: FlatHat[],
  fancyHats: FancyHat[],
}

etc. Each hat would be a simple object type - if I'm understanding correctly, knowing what the structure of that further is not needed to answer this question, as the function will take care of that.


In fact there's one piece of implementation missing - the hat map!

interface HatMap extends Record<HatType, string> {
  [HatType.Big]: 'bigHats',
  ...
}

which may be useful to generate the return type (such as it is here). Please feel free to create your own map if required to answer the question


In the worst case, the above would be dependent upon some property of the type. Each Hat type (e.g., BigHat) would extend the following interface:

interface IHat {
  kind: HatType;
}

CodePudding user response:

Is it possible to type the return value based upon the enum values I've passed in?

Only if the enum values passed in are compile-time constants. When the type is just HatType, you'll get the entirety of AllHats.

...which I couldn't find a way to do with a function parameter.

You can do that with a generic type parameter.

I've come up with two ways to do it.

The first way

The first way uses AllHats and HatMap from the question (but I know you've said they're just examples). Since we need to pick properties from AllHats based on their value type, we need something like this to do that:

// A type that picks out the keys for properties from `Source` that are
// assignable to `PickType`
type KeysByType<Source extends object, PickType> = {
    [Key in keyof Source]: Source[Key] extends PickType ? Key : never;
}[keyof Source];

// A type that picks the properties from `Source` that are assignable to `PickType`
type PickByType<Source extends object, PickType> =
    Pick<Source, KeysByType<Source, PickType>>;

As you said, we can map the generic type parameter from HatType to the type of hat via HatMap, then we can use pick out the properties of AllHats based on those types:

const fn = <T extends HatType>(types: T[]): PickByType<AllHats, HatMap[T][]> => {
    // ...implementation...
};

Test cases:

const hatResults = fn([ HatType.Big ]);
hatResults.bigHats
hatResults.flatHats // <=== Example error, no `flatHats` on it because `HatType.Flat` wasn't included
const moreHatResults = fn([ HatType.Flat ]);
moreHatResults.flatHats
const twoHatsResults = fn([ HatType.Big, HatType.Flat ]);
twoHatsResults.bigHats
twoHatsResults.flatHats

declare let e1: HatType;
declare let e2: HatType;
const unknownHats = fn([e1, e2]);
unknownHats.bigHats
unknownHats.flatHats
unknownHats.fancyHats

Playground link

That works, but the type hint you get for (say) twoHatsResults above is just PickByType<AllHats, (BigHat | FlatHat)[]>, which isn't all that helpful.

The second way

There's another way —and it's much simpler — if we redefine HatMap as a union of the object types we want (and then we don't need AllHats at all):

type HatMap = {
    [HatType.Big]: {bigHats: BigHat[]},
    [HatType.Flat]: {flatHats: FlatHat[]},
    [HatType.Fancy]: {fancyHats: FancyHat[]},
}

Now if we define fn as returning HatMap[T], we'll get a union of those object types — {bigHats: BigHat[]} | {flatHats: FlatHat[]} for instance. But we don't want a union, we want an intersection.

jcalz to the rescue:

type UnionToIntersection<U> = 
    (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

So now fn is:

const fn = <T extends HatType>(types: T[]): UnionToIntersection<HatMap[T]> => {
    // ...implementation...
};

When we throw our test cases at it, the type hints are more useful:

const hatResults = fn([ HatType.Big ]);
//    ^? { bigHats: BigHat[]; }
hatResults.bigHats
hatResults.flatHats // <=== Example error, no `flatHats` on it because `HatType.Flat` wasn't included
const moreHatResults = fn([ HatType.Flat ]);
//    ^? { flatHats: FlatHat[]; }
moreHatResults.flatHats
const twoHatsResults = fn([ HatType.Big, HatType.Flat ]);
//    ^? { bigHats: BigHat[]; } & { flatHats: FlatHat[]; }
twoHatsResults.bigHats
twoHatsResults.flatHats

declare let e1: HatType;
declare let e2: HatType;
const unknownHats = fn([e1, e2]);
//    ^?
//    { bigHats: BigHat[]; } & { flatHats: FlatHat[]; } & { fancyHats: FancyHat[]; }
unknownHats.bigHats
unknownHats.flatHats
unknownHats.fancyHats

{ bigHats: BigHat[]; } & { flatHats: FlatHat[]; } isn't as nice as { bigHats: BigHat[]; flatHats: FlatHat[]; } would have been, but it's much more informative than PickByType<AllHats, (BigHat | FlatHat)[]>.

Playground link

  • Related