Search code examples
typescript

How to make function arguments arrays that not intersect?


At all, my aim is to make a function that takes 2 string arrays as arguments and the second one should not include elements of the first:

function doSomething<
    const T extends string,
    const K extends StringExcept<T>
>(
    firstArray: T[],
    secondArray: K[]
) {}

doSomething(['a', 'b'], ['c', 'b'])  // 'b' in the second array should cause type error

Firstly, I thought, that I need a type type like type StringExcept<T>, which should work this way:

type NotAbc = StringExcept<'abc'>;
const notAbcArray:NotAbc[] = [
    'any string',   // okay
    '123',          // okay
    'abc'           // not allowed
]

What i have tried is using Exclude<> type, like Exclude<string, 'abc'>, but it does not work like this, and gives just string type. Also I have tried different variations like T extends K ? never : T statement, but then I found out that type Exclude does literally the same

Then I have found this question, which says that type like StringExcept is impossible in TS for now (but if there are some updates, I would like to know). Also I have seen this experimental feature, that is closed now.

So, is there a way to make my doSomething(firstArray, secondArray) function, using type like StringExcept or not?


Solution

  • You could use the Exclude utility type within the type of secondArray itself:

    function doSomething<
      const T extends string,
      const U extends string
    >(
      firstArray: T[],
      secondArray: Exclude<U, T>[]
    ) { }
    

    This will cause U to be inferred as the elements of secondArray, and then checked against Exclude<U, T>:

    doSomething(['a', 'b'], ['c', 'd']); // okay
    
    doSomething(['a', 'b'], ['c', 'b']); // error! 
    // -------------------------> ~~~
    // Type '"b"' is not assignable to type '"c"'.
    

    This works because both T and U will (hopefully) be a union of string literal types, and Exclude can filter such unions (whereas it cannot filter non-union types like string itself).

    It would be even nicer if we could put Exclude<U, T> in the constaint for U instead, like const U extends string & Exclude<U, T>, but that is seen by the compiler as illegally circular. So we can move it down into the parameter type.

    Playground link to code