Enforce that an array is exhaustive over a union type

后端 未结 2 1455
长发绾君心
长发绾君心 2021-01-13 20:23

Given a strongly-typed tuple created using a technique such as described here:

const tuple = (...args: T) => args;
const furnitu         


        
相关标签:
2条回答
  • 2021-01-13 20:44

    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

    0 讨论(0)
  • 2021-01-13 20:54

    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

    0 讨论(0)
提交回复
热议问题