Home > Software engineering >  Mutually exclusive types in a function signature
Mutually exclusive types in a function signature

Time:05-07

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
}

CodePudding user response:

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.

  • Related