3

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.

6
  • 1
    I don't think you can identify an enum programmatically. The closest I've ever gotten is something like type IsEnum<T> = T extends Record<keyof T, string | number> ? true : false, but in any case you'd need to do EnumOrString<typeof Features>. Commented May 21, 2018 at 15:39
  • Wait, so you want something that takes a data structure and recursively changes Features to Features | keyof typeof Features? Commented May 21, 2018 at 15:44
  • well, members of any enum type (recursively) - but not all members will be enum typed Commented May 21, 2018 at 15:46
  • requested in github.com/Microsoft/TypeScript/issues/24293 Commented May 21, 2018 at 15:56
  • There's two parts to this question, then... one which is "identify enums", which I don't think you can do. The other is "recursively replace an enum type with the union of the enum values and the keys of the enum object", which you can do if you know in advance which enums you care about (e.g., Features). Commented May 21, 2018 at 15:58

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.