enum E {
A = 'A',
B = 'B'
}
type O = keyof typeof E
const state: { [key in keyof typeof E]?: string } = {};
If you want to make state
mutable, just use -readonly
:
const state: { -readonly [key in keyof typeof E]?: string } = {};
How can I constrain object keys to enum values while their presence is not required and value is not undefined?
Could you please provide some pseudo code?
Because, if key is not required, and you will try to get the value of non existence key, according to JS standard you should receive undefined
UPDATE
enum E {
A = 'A',
B = 'B',
C = 'C',
D = 'D'
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// //https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type State<T extends string, R extends string[] = []> = {
[P in T]: IsUnion<T> extends true ? State<Exclude<T, P>, [...R, Exclude<T, P>]> : R
}[T]
// Array of all possible keys
type Result = UnionToArray<State<keyof typeof E>>
// convert union to object with appropriate keys
type MapPredicate<T> = UnionToIntersection<T extends string ? {
[P in T]: string
} : never>
// don't allow empty object because value can't be undefined
type Empty = { __tag: 'empty' }
// iterate through array of strings
type MappedString<
Arr,
Result = Empty
> = Arr extends []
? []
: Arr extends [infer H]
? Result | MapPredicate<H>
: Arr extends [infer Head, ...infer Tail]
? MappedString<[...Tail], Result | MapPredicate<Head>>
: Readonly<Result>;
// iterate through array of array of string
type MappedArray<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MappedString<H>]
: Arr extends [infer Head, ...infer Tail]
? MappedArray<[...Tail], [...Result, MappedString<Head>]>
: Readonly<Result>;
type AllPossibleValues = MappedArray<Result>[number];
const A: AllPossibleValues = { A: 'A' }
const AB: AllPossibleValues = { A: 'A', B: 'B' }
const ABC: AllPossibleValues = { A: 'A', B: 'B', C: 'C' }
const ABCD: AllPossibleValues = { A: 'A', B: 'B', C: 'C', D: 'D' }
const CD: AllPossibleValues = { C: 'C', D: 'D' }
const AD: AllPossibleValues = { A: 'A', D: 'D' }
const BD: AllPossibleValues = { B: 'B', D: 'D' }
const BE: AllPossibleValues = {} // expected error
const QA: AllPossibleValues = {A:'A', Q:'Q'} // expected error
const state:AllPossibleValues={A:'A'}
const x = Object.entries(state).forEach(([key, value]) => { /* [key: string, value: string] */
})
Pros: No assertions, no type castings
Cons: I have to similar Mapped utils which I don't know how to refactor. But it doe not affect your compiled code anyway. Also, if you add 5th property to enum, above code will not compile, because of recursion restrictions :)
So, if your object has less then 5 props, you are good to go.
TypeScript allows you roughly ~50 recursion calls.
If you have obj with 5 props, you should create union with parseInt('11111',2)
31 items. I think because my rec MappedArray
calls rec MappedString
I reached this limit faster.
Playground link