Oh, the type-level equality operator as mentioned in microsoft/TypeScript#27024. @MattMcCutchen has introduced a solution detailed in a comment on that issue, which involves using generic conditional types to determine when two types are exactly identical rather than just assignable to each other. In an ideal type system, "mutually assignable" and "equal" would be indistinguishable, but TypeScript doesn't uphold this perfectly. Specifically, the any
type can be assigned to or from any other type, resulting in situations where
string extends any ? true : false
and
any extends string ? true: false
both yield
true
, despite the fact that
string
and
any
aren't the same.
Here's an implementation of IfEquals<T, U, Y, N>
which returns Y
if T
and U
are equal, and N
otherwise.
type IfEquals<T, U, Y=unknown, N=never> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2) ? Y : N;
Let's test it out:
type EQ = IfEquals<any[], [number][], "same", "different">; // "different"
Indeed, those are recognized as distinct types. There may be other scenarios where seemingly identical types are treated differently, and vice versa:
type EQ1 = IfEquals<
{ a: string } & { b: number },
{ a: string, b: number },
"same", "different">; // "different"!
type EQ2 = IfEquals<
{ (): string, (x: string): number },
{ (x: string): number, (): string },
"same", "different">; // "different", as expected, but:
type EQ3 = IfEquals<
{ (): string } & { (x: string): number },
{ (x: string): number } & { (): string },
"same", "different">; // "same"!! however they are not equivalent.
// Function intersections are order-dependent
With this type in place, we can create a function that throws an error unless the two types are equal in this manner:
/** Trigger a compiler error when a value is _not_ an exact type. */
declare const exactType: <T, U>(
draft: T & IfEquals<T, U>,
expected: U & IfEquals<T, U>
) => IfEquals<T, U>
declare let a: any[]
declare let b: [number][]
// $ExpectError
exactType(a, b) // error
Each argument is combined with IfEquals<T, U>
to ensure there is an error unless T
and U
are identical. This provides the desired outcome, I believe.
Note that optional parameters for this function have been omitted. The absence of these optional parameters could potentially weaken the check:
declare let c: string | undefined
declare let d: string
exactType(c, d) // no error if optional parameters!
You can decide whether this is significant or not.