To tackle this issue, one approach could be to overload the signature and get it over with.
One possible method is to utilize 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 can yield detailed error messages, but the readability of the function signature may suffer.
Alternatively, you can follow the conditional route as proposed in your initial query:
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 might be even more obscure, and the error message might not provide much insight. It can be perplexing to encounter a parameter being labeled as never
.
Another option is to employ 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.
Using overloads remains the conventional way to manage polymorphic function signatures in TypeScript.