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
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.
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)[]>
.