Typescript enum type check for conditional types?

Jason Kleban picture Jason Kleban · May 21, 2018 · Viewed 7.8k times · Source

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.

Answer

jcalz picture jcalz · May 21, 2018

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.