Reputation: 13456
There are a lot of similar issues here but they don't cover the checklist at the end of the post (especially the "value being undefined" part).
I have an enum
enum E {
A = 'A',
B = 'B'
}
Then I wanted to constrain my object value in state to { [key in E]: string }
but that requires me to instantiate the object already will all enum's keys. This is not allowed:
const state: { [key in E]: string } = {};
Type '{}' is missing the following properties from type '{ A: string; B: string; }': A, B
So I tried to constrain it like this { [key in E]?: string }
. That allows me to omit enum keys, thus allows to instantiate empty object {}
and also checks that key values are in enum's range:
state.A = 'x'; // ok
state.C = 'y'; // gives error which is nice
But then I encountered issue when forEaching Object entries
Object.entries(state).forEach(([key, value]) => console.log(value === undefined));
Typescript thinks that value
can be of type string|undefined
but that is never true.
When using only string as a key, the value is not considered undefined
const state: { [key: string]: string } = {};
How can I constrain object keys to enum values while their presence is not required and value is not undefined?
Checklist:
state.A = 'x' // ok
state.C = 'x' // error
Object.entries(state).forEach(([key, value])=>console.log(value.charAt(0))); // ok, no Object is possibly 'undefined'.(2532) error
TLDR: I want to get rid of the assert
in the Example here
Upvotes: 5
Views: 6334
Reputation: 42188
As far as typescript is concerned, having A
omitted from the object is the same as having A
explicitly set to undefined
. It's fine to do this:
const state: { [key in E]?: string } = {};
state.A = undefined;
@captain-yossarian has given you a good but very complicated way to prevent this behavior. The easier thing to do is to simply filter out any undefined
values.
This callback is a type guard which makes sure that the value
in a [key, value]
tuple is defined.
const isDefinedValue = <T,>(tuple: [string, T | undefined]): tuple is [string, Exclude<T, undefined>] => {
return tuple[1] !== undefined;
}
We can then filter the Object.entries
before using it.
Object.entries(state)
.filter(isDefinedValue)
.forEach(([key, value])=>console.log(value.charAt(0))); // value is `string`
Upvotes: 6
Reputation: 33051
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.
Upvotes: 2