Reputation: 33637
I have an object with fixed keys that I use as an enum-like object. The values are variable length arrays. This works as desired. However it requires duplicating the same type assertion on every value in the object. Is there a way to type them all at once instead of individually?
const Direction = 'u' | 'r' | 'd' | 'l'
const gestures = {
newSubThought: ['r', 'd', 'r'] as Direction[],
newThought: ['r', 'd'] as Direction[],
clearThought: ['l', 'r'] as Direction[],
...
}
If I omit as Direction[]
then the values are too wide, not allowing me to pass gestures.newThought
as a Direction[]
.
The type 'string' is not assignable to type 'Direction'.
If I widen the keys to { [key: string]: Direction[] }
then I lose the narrow keys and the autocompletion that comes with it.
Upvotes: 1
Views: 370
Reputation: 328097
There is an open issue at microsoft/TypeScript#25214 requesting the ability to annotate the type of the values of an object, while allowing the compiler to somehow infer the keys. For now there is no direct support for this; and the issue is marked as "needs proposal", meaning that it's not clear how such a feature would work. If you're really interested in seeing this happen, you could give that issue a 👍 and possibly suggest how it would work.
For now, though, you can get indirect support for this via a generic identity helper function:
const asGestures = <K extends PropertyKey>(
gestures: { [P in K]: Direction[] }) => gestures;
Instead of annotating or asserting that each property of your object literal is a Direction[]
, you call asGestures()
on the "plain" object literal. The compiler will constrain the type of the object to something whose values are of type Direction[]
, and whose keys are of type a key-like type K
it infers:
const gestures = asGestures({
newSubThought: ['r', 'd', 'r'],
newThought: ['r', 'd'],
clearThought: ['l', 'r']
//...
})
/* const gestures: {
newSubThought: Direction[];
newThought: Direction[];
clearThought: Direction[];
} */
Even better, if you make a mistake in your array, the compiler will notice and catch it for you:
const badGestures = asGestures({
begoneThought: ['r', 'u', 'd', 'e'], // error!
// --------------------------> ~~~
// Type '"e"' is not assignable to type 'Direction'.
})
whereas type assertions using as
will tend to suppress such errors (one major use of type assertions is to narrow types past what the compiler can verify):
['r', 'u', 'd', 'e'] as Direction[] // no error
Upvotes: 1
Reputation: 33637
The best solution I could find was using mapped types:
// define an enum-like object with the exact values
// (optionally add `as const` for readonly keys)
const gestureEnum = {
newSubThought: ['r', 'd', 'r'],
newThought: ['r', 'd'],
clearThought: ['l', 'r'],
...
}
// widen the type of the gestureEnum values to Direction[] using a mapped type
export const gestures = gestureEnum as {
[key in keyof typeof gestureEnum]: Direction[]
}
/* The final type:
const gestures: {
newSubThought: Direction[],
newThought: Direction[],
clearThought: Direction[],
...
}
*/
Upvotes: 0