Given a strongly-typed tuple created using a technique such as described here:
const tuple = <T extends string[]>(...args: T) => args;
const furniture = tuple('chair', 'table', 'lamp');
// typeof furniture[number] === 'chair' | 'table' | 'lamp'
I want to assert at design time that it's exhaustive over another union type:
type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman'
How can I create a type that will ensure that furniture
contains each and only the types in the Furniture
union?
The goal is to be able to create an array at design time like this, and have it fail should Furniture
change; an ideal syntax might look like:
const furniture = tuple<Furniture>('chair', 'table', 'lamp')
It's a mapped type containing one property for each type in Union , which is a function from that specific type to a string . This is exhaustive: if you leave out one of A or B , or put the B function on the A property, the compiler will complain.
A union type describes a value that can be one of several types. We use the vertical bar ( | ) to separate each type, so number | string | boolean is the type of a value that can be a number , a string , or a boolean .
The "property does not exist on type union" error occurs when we try to access a property that is not present on every object in the union type. To solve the error, use a type guard to ensure the property exists on the object before accessing it.
The in operator narrowing JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types. For example, with the code: "value" in x . where "value" is a string literal and x is a union type.
TypeScript doesn't really have direct support for an "exhaustive array". You can guide the compiler into checking this, but it might be a bit messy for you. A stumbling block is the absence of partial type parameter inference (as requested in microsoft/TypeScript#26242). Here is my solution:
type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman';
type AtLeastOne<T> = [T, ...T[]];
const exhaustiveStringTuple = <T extends string>() =>
<L extends AtLeastOne<T>>(
...x: L extends any ? (
Exclude<T, L[number]> extends never ?
L :
Exclude<T, L[number]>[]
) : never
) => x;
const missingFurniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp');
// error, Argument of type '"chair"' is not assignable to parameter of type '"ottoman"'
const extraFurniture = exhaustiveStringTuple<Furniture>()(
'chair', 'table', 'lamp', 'ottoman', 'bidet');
// error, "bidet" is not assignable to a parameter of type 'Furniture'
const furniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp', 'ottoman');
// okay
As you can see, exhaustiveStringTuple
is a curried function, whose sole purpose is to take a manually specified type parameter T
and then return a new function which takes arguments whose types are constrained by T
but inferred by the call. (The currying could be eliminated if we had proper partial type parameter inference.) In your case, T
will be specified as Furniture
. If all you care about is exhaustiveStringTuple<Furniture>()
, then you can use that instead:
const furnitureTuple =
<L extends AtLeastOne<Furniture>>(
...x: L extends any ? (
Exclude<Furniture, L[number]> extends never ? L : Exclude<Furniture, L[number]>[]
) : never
) => x;
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With