I have restful services that accept enum values as either the number OR the string, but always return just the number. Is there a way to type them?
Here's what I kinda want, but it is not syntactically valid:
enum Features {
"A" = 1,
"B" = 2,
"C" = 2
}
type EnumOrString<T> = T extends enum
? T | keyof T
: T
declare function getData(featureFilter: EnumOrString<Features>[]): Features[]
getData
takes an array of the enum values or the enum keys but returns only the enum values.
I also would want to extend this to mapped types like DeepPartial
so that any nested enums all get this treatment - without having to have separate hierarchies of types partitioned by Request and Response.
I don't think the "identify an enum
" thing is possible right now. Even if you could, you can't programmatically convert from the Features
type (which is an element of the Features
enumeration) to the typeof Features
type (the mapping from keys to Features
elements) without knowing about Features
in the first place. Again: the type Features.B
for example, doesn't know anything about the string literal "B"
. Only typeof Features
has a property like {B: Features.B}
. If you want a type function to convert from Features
to Features | keyof typeof Features
, you need to mention typeof Features
explicitly. So even if you had your dream extends enum
notation, you'd still need to write the replacement code with a list of mappings you care about. Sorry.
Addressing just the recursion part, in case it matters, here's how I'd recursively process a type to replace a known enum value with the union of the enum values and the relevant keys:
enum Features {
"A" = 1,
"B" = 2,
"C" = 2
}
type ValueOf<T> = T[keyof T]
type FeatureKey<T extends Features> =
Extract<ValueOf<{
[K in keyof typeof Features]: [K, typeof Features[K]]
}>, [any, T]>[0]
type DeepFeaturesOrKey<T> =
T extends Features ? (T | FeatureKey<T>) :
T extends Array<infer L> ? DeepFeaturesOrKeyArray<L> :
T extends object ? { [K in keyof T]: DeepFeaturesOrKey<T[K]> } : T
interface DeepFeaturesOrKeyArray<L> extends Array<DeepFeaturesOrKey<L>> { }
The tricky bits are extracting a subset of the enum if you don't specify the whole thing (e.g., you're using a discriminated union keyed off a specific enum value), and of course, the whole deep-whatever Array trickery to avoid the dreaded "circular reference" error message mentioned here:
Similar to union and intersection types, conditional types are not permitted to reference themselves recursively (however, indirect references through interface types or object literal types are allowed)
Let's test it:
interface Foo {
bar: string,
baz: Features,
qux: {
a: Features[],
b: boolean
},
quux: Features.A,
quuux: Features.B
}
type DeepFeaturesOrKeyFoo = DeepFeaturesOrKey<Foo>
declare const deepFeaturesOrKeyFoo: DeepFeaturesOrKeyFoo
deepFeaturesOrKeyFoo.bar; // string
deepFeaturesOrKeyFoo.baz; // Features | "A" | "B" | "C"
deepFeaturesOrKeyFoo.qux.a[1]; // Features | "A" | "B" | "C"
deepFeaturesOrKeyFoo.qux.b; // boolean
deepFeaturesOrKeyFoo.quux; // Features.A | "A"
deepFeaturesOrKeyFoo.quuux; // Features.B | "B" | "C"
// note that the compiler considers Features.B and Features.C to be the
// same value, so this becomes Features.B | "B" | "C"
Looks good. Hope that helps.