Typescript type guard for requiring all elements

前端 未结 3 1946
旧时难觅i
旧时难觅i 2021-01-21 17:54

Is there a way to require array elements with typescript so that I can do have

type E = keyof T; // Number of properties in T is unknown

let T

3条回答
  •  伪装坚强ぢ
    2021-01-21 18:49

    I'm going to define this:

    type Arg = T extends any ? { arg: T } : never;
    

    so that we can use Arg (equivalent to {arg:"el1"}|{arg:"el2"}|{arg:"el3"}) in what follows.


    The best you can hope for here would be some generic helper function verifyArray() which would enforce the restrictions that its argument is:

    • an array of elements from a union
    • missing no elements from the union
    • and containing no duplicates

    And it's going to be ugly.


    There's no usable concrete type that will enforce this for unions containing more than about six elements. It is possible to use some illegally-recursive or legally-nonrecursive-but-tedious type definitions to take a union type like 0 | 1 | 2 | 3 and turn it into a union of all possible tuples that meet your criteria. That would produces something like

    type AllTuples0123 = UnionToAllPossibleTuples<0 | 1 | 2 | 3>
    

    which would be equivalent to

    type AllTuples0123 =
        | [0, 1, 2, 3] | [0, 1, 3, 2] | [0, 2, 1, 3] | [0, 2, 3, 1] | [0, 3, 1, 2] | [0, 3, 2, 1]
        | [1, 0, 2, 3] | [1, 0, 3, 2] | [1, 2, 0, 3] | [1, 2, 3, 0] | [1, 3, 0, 2] | [1, 3, 2, 0]
        | [2, 0, 1, 3] | [2, 0, 3, 1] | [2, 1, 0, 3] | [2, 1, 3, 0] | [2, 3, 0, 1] | [2, 3, 1, 0]
        | [3, 0, 1, 2] | [3, 0, 2, 1] | [3, 1, 0, 2] | [3, 1, 2, 0] | [3, 2, 0, 1] | [3, 2, 1, 0]
    

    But for an input union of n elements that would produce an output union of n! (that's n factorial) outputs, which grows very quickly in n. For your example "el1"|"el2"|"el3" it would be fine:

    type AllPossibleTuplesOfArgs = UnionToAllPossibleTuples>;
    const okay: AllPossibleTuplesOfArgs = 
      [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el3" }]; // okay
    const bad1: AllPossibleTuplesOfArgs = [{ "arg": "el1" }, { "arg": "el2" }]; // error!
    const bad2: AllPossibleTuplesOfArgs = // error!
      [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }];
    const bad3: AllPossibleTuplesOfArgs = 
      [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]  // error!
    

    but I assume you want something that doesn't crash your compiler when your object has seven or more properties in it. So let's give up on UnionToAllPossibleTuples and any concrete type.


    So what would verifyArray() look like?

    First let's make a type function called NoRepeats which takes a tuple type T and returns the same thing as T if and only if T has no repeated elements... otherwise it returns a modified tuple to which T is not assignable. This will allow us to make the constraint T extends NoRepeats to say "the tuple type T has no repeated elements". Here's a way to do it:

    type NoRepeats = { [M in keyof T]: { [N in keyof T]:
        N extends M ? never : T[M] extends T[N] ? unknown : never
    }[number] extends never ? T[M] : never }
    

    So NoRepeats<[0,1,2]> is [0,1,2], but NoRepeats<[0,1,1]> is [0,never,never]. Then verifyArray() might be written as this:

    const verifyArray = () =>  & readonly T[]>(
        u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
    ) => u;
    

    It takes a type T to check against, and returns a new function which makes sure its argument has no repeats (from U extends NoRepeats), is assignable to T[] (from & readonly T), and not missing any elements of T (from & ([T] extends [U[number]] ? unknown : never)). Yes it's ugly. Let's see if it works:

    const verifyArgEArray = verifyArray>()
    const okayGeneric = verifyArgEArray([{ "arg": "el3" }, { "arg": "el1" }, { "arg": "el2" }]); // okay
    const bad1Generic = verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }]); // error
    const bad2Generic = // error
        verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }]);
    const bad3Generic = // error
        verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]);
    

    So that works.


    Both of these force you to fight with the type system. You could possibly make a builder class as in this answer which plays more nicely with the type system but involves even more runtime overhead, and is arguably only slightly less ugly.


    Honestly I'd suggest trying to refactor your code not to require TypeScript to enforce this. The easiest thing is to require an object have these values as keys (e.g., just make a value of type T or possibly Record) and use that instead of (or before producing) an array. Oh well, hope this helps. Good luck!

    Playground link to code

提交回复
热议问题