Given a strongly-typed tuple created using a technique such as described here:
const tuple = (...args: T) => args;
const furnitu
There are ways of doing this but it might be a bit messy for you. The two stumbling blocks here are the absence of partial type parameter inference and invalid types. Here is my solution:
type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman';
const exhaustiveStringTuple = <T extends string>() =>
<L extends T[]>(
...x: L & ([T] extends [L[number]] ? L : [
Error, "You are missing ", Exclude<T, L[number]>])
) => x;
const missingFurniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp');
// error, [string, string, string] is not assignable to parameter of type
// ["chair", "table", "lamp"] & [Error, "You are missing", "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 Furniture[]>(
...x: L & ([Furniture] extends [L[number]] ? L : [
Error, "You are missing ", Exclude<Furniture, L[number]>])
) => x;
const missingFurniture = furnitureTuple('chair', 'table', 'lamp');
// error, [string, string, string] is not assignable to parameter of type
// ["chair", "table", "lamp"] & [Error, "You are missing", "ottoman"]
const extraFurniture = furnitureTuple('chair', 'table', 'lamp', 'ottoman', 'bidet');
// error, "bidet" is not assignable to a parameter of type 'Furniture'
const furniture = furnitureTuple('chair', 'table', 'lamp', 'ottoman');
// okay
The other issue is that the error you get when you leave out a required argument is
I have other proposition
type RemoveFirstFromTuple<T extends any[]> =
T extends [] ? undefined :
(((...b: T) => void) extends (a: any, ...b: infer I) => void ? I : [])
const tuple = <T extends string[]>(...args: T) => args;
type FurnitureUnion = 'chair' | 'table' | 'lamp';
type FurnitureTuple = ['chair', 'table' , 'lamp'];
type Check<Union, Tuple extends Array<any>> = {
"error": never,
"next": Check<Union, RemoveFirstFromTuple<Tuple>>,
"exit": true,
}[Tuple extends [] ? "exit" : Tuple[0] extends Union ? "next" : "error"];
type R = Check<FurnitureUnion, FurnitureTuple>; // true
type R1 = Check<'chair' | 'lamp' | 'table', FurnitureTuple>; // true
type R2 = Check<'chair' | 'lamp' | 'table', ['chair', 'table' , 'lamp', 'error']>; // nerver
Remove from tuple takes tuple and return tuple without first element (will be needed later)
Check will iterate on Tuple. Each step can return never
when Tuple[0] don't extend Union, exit when input tuple is empty and next when Tuple[0] extends Union. In next step we recursive call Check but first we remove first element from Tuple by previous util
Playground