Reputation: 731
I'm trying to create a simple switch function which takes a first parameter that must be an union of string & an object which have keys based on the first parameter union and can return any value.
export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
return possibilities[value];
};
Typical usage would be
let option: "val1" | "val2" | "val3" = "val1";
// should returns s1
// Impossible should be type-checked as an error since it's not part of the option union type
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"});
My problem occurs because the generic type T
must be a string
in order to be used as an object key. I don't know how you can tell T
to be an union of string
.
I tried T extends string
with no success.
Upvotes: 12
Views: 36535
Reputation: 250366
The T extends string
version seems to work well. It disallows impossible
, but wouldn't you want to disallow it since if the parameter can never have that value that option would be useless?:
export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
return possibilities[value];
};
declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"});
If you want to allow the extra keys you could declare the case object separately (bypassing excess property checks and allowing you to reuse the case object)
declare let option: "val1" | "val2" | "val3";
const casses = {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}
mySwitch(option, casses);
Or you could change your type a little bit so the generic type parameter is the case object, and the value will the be typed as keyof T
:
export const mySwitch = <T>(value: keyof T, possibilities: T): any => {
return possibilities[value];
};
declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"});
Also a better option would be to preserve the type from the case object instead of using any
:
export const mySwitch = <T, K extends keyof T>(value: K, possibilities: T): T[K] => {
return possibilities[value];
};
declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: 1, val2: "s2", val3: "s3", impossible: false}); // returns string | number
Edit:
To preserve both correct return type and error if there are possibilities not present in union you could use this:
const mySwitch = <T extends Record<K, any>, K extends string>(value: K, possibilities: T & Record<Exclude<keyof T, K>, never>): any => {
return possibilities[value];
};
let option: "val1" | "val2" | "val3" = (["val1", "val2", "val3"] as const)[Math.round(Math.random() * 2)]
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3" });
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "" }); //err on impossible
Note that because typescript does control flow analysis you need to make sure option
is not just types as the actual constant you assign instead of the type annotation you specify
Upvotes: 10