'Mutually exclusive types in a function signature

Is this possible to achieve with conditional types in TypeScript?

type Type1 = {
  field: string,
}

type Type2 = {
  field: number,
}

// I would like to make sure that if arg1 is Type1 then arg2 is Type2
// and vice versa
const func = (arg1: Type1 | Type2, arg2: Type1 | Type2) {
  // do something
}


Solution 1:[1]

There are a few ways you could approach this, but you'll probably find it's easier to just overload the signature and be done with it.

You could use a union of tuples:

const func = (...args: [arg1: Type1, arg2: Type2] | [arg1: Type2, arg2: Type1]) => {
    const arg1 = args[0] 
      // Type1 | Type2
    const arg3 = args[2] 
      // Property '2' does not exist on type ...
    return;
}

func({field: 32}, {field: "hi"}) 
  // ok
func({field: "hi"}, {field: 32}) 
  // ok
func({field: 32}, {field: 2}) 
  // Type at position 1 in source is not compatible with type at position 1 in target.

This provides fairly descriptive error messages, but the function signature is not easy to read in my opinion.

You could go the conditional route as suggested in your original question:

type Exclusive<A, B, T1, T2> = 
    A extends T1
        ? B extends T2
            ? A : never 
    : A extends T2
        ? B extends T1
            ? A : never
    : never;

type ExclusiveTypes<A, B> = Exclusive<A, B, Type1, Type2>

const func = <T, U>(arg1: ExclusiveTypes<T, U>, arg2: ExclusiveTypes<U, T>) => {
    return;
}

func({field: 32}, {field: "hi"}) 
  // ok
func({field: "hi"}, {field: 32}) 
  // ok
func({field: 32}, {field: 2}) 
  // Type 'number' is not assignable to type 'never'.

The signature here is even more obscure to me, and the error message is not very useful at all. It's confusing to be told that a parameter is never.

Or you could use overloads:

function func(arg1: Type1, arg2: Type2): void;
function func(arg1: Type2, arg2: Type1): void;
function func(arg1: Type1 | Type2, arg2: Type1 | Type2) {
    return
}

func({field: 32}, {field: "hi"}) 
  // ok
func({field: "hi"}, {field: 32}) 
  // ok
func({field: 32}, {field: 2})
  // No overload matches this call.

This is still the idiomatic way to handle polymorphic function signatures in TypeScript.

Solution 2:[2]

Yet another solution but this one does not use overloads or is similar to the conditional types provided by @lawrence-witt.

function func<A extends Type1 | Type2>(arg1: A, arg2: Exclude<Type1 | Type2, A>) { ... }

You can retain the type of the first argument using a generic, then for the type of the second one, exclude the first argument's type, giving you the other constituents.

Playground with example calls

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2