How to test if two types are exactly the same
Asked Answered
I

7

44

Here is my first attempt: (playground link)

/** Trigger a compiler error when a value is _not_ an exact type. */
declare const exactType: <T, U extends T>(
    draft?: U,
    expected?: T
) => T extends U ? T : 1 & 0

declare let a: any[]
declare let b: [number][]

// $ExpectError
exactType(a, b)

Related: https://github.com/gcanti/typelevel-ts/issues/39

Inmesh answered 16/12, 2018 at 23:52 Comment(3)
Just curious, why would you do that? Any use case?Ostap
@Ostap For testing ambient type definitionsInmesh
As typescript uses structural-type-system, I'm having trouble understanding the reasoning of this type of assertions. Could you, please, elaborate on your case?Guntar
L
67

Ah, the type-level equality operator as requested in microsoft/TypeScript#27024. @MattMcCutchen has come up with a solution, described in a comment on that issue involving generic conditional types which does a decent job of detecting when two types are exactly equal, as opposed to just mutually assignable. In a perfectly sound type system, "mutually assignable" and "equal" would probably be the same thing, but TypeScript isn't perfectly sound. In particular, the any type is both assignable to and assignable from any other type, meaning that string extends any ? true : false and any extends string ? true: false both evaluate to true, despite the fact that string and any are not the same type.

Here's an IfEquals<T, U, Y, N> type which evaluates to 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 see it work:

type EQ = IfEquals<any[], [number][], "same", "different">; // "different"

Okay, those are recognized as different types. There are probably some other edge cases where two types that you think are the same are seen as different, 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"!! but they are not the same, 
// intersections of functions are order-dependent

Anyway, given this type we can make a function that generates an error unless the two types are equal in this way:

/** 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 has a type T or U (for type inference of the generic parameter) intersected with IfEquals<T, U> so that there will be an error unless T and U are equal. This gives the behavior you want, I think.

Note that the arguments of this function are not optional. I don't really know why you wanted them to be optional, but (at least with --strictNullChecks turned on) it weakens the check to do so:

declare let c: string | undefined
declare let d: string
exactType(c, d) // no error if optional parameters!

It's up to you if that matters.

Laminous answered 17/12, 2018 at 2:7 Comment(9)
This knowledge will help millions of people. Thanks @jcalz!Inmesh
Why is expected: U & IfEquals<T, U> necessary? I think draft: T & IfEquals<T, U> catches all the errors.Inmesh
I simplified it to this: gist.github.com/aleclarson/a5e5efefb907a22aef64a00f8d7c7416Inmesh
I think if you replace U & IfEquals<T, U> with just U then exactType() will accept arguments where T is never and U is something else, and I assumed you wanted that to be excluded.Laminous
Your solution fails when comparing any[] with ReadonlyArray<any> (see here: bit.ly/2TNxPop). I've updated the gist above with a solution that catches this.Inmesh
Could somebody explain me why we use extra <G>() => G extends T helper? Just don't understand itHouseman
@captain-yossarianfromUkraine this comment on the linked github issue explains it: github.com/Microsoft/TypeScript/issues/…Klaxon
Should { a: string } & { b: number } and { a: string, b: number } be considered the same?Tremolant
In a perfectly sound type system, "mutually assignable" and "equal" would probably be the same thing maybe you meant nominal-type-system X structural-type-system here?Guntar
Y
12

If you are looking for a pure typescript solution without any third-party library dependency, this one should work for you

export function assert<T extends never>() {}
type TypeEqualityGuard<A,B> = Exclude<A,B> | Exclude<B,A>;

And usage like

assert<TypeEqualityGuard<{var1: string}, {var1:number}>>(); // returns an error
assert<TypeEqualityGuard<{var1: string}, {var1:string}>>(); // no error
Yount answered 23/8, 2022 at 15:47 Comment(1)
This is super cool. I wanted to make sure all properties in an object were being destructured/used, and I did this const { prop1, prop2, ...rest } = someObject; const allPropsUsed = <T extends Record<PropertyKey, never>>(rest: T) => { if (Object.keys(rest).length) throw Error("Not all props used"); }; allPropsUsed(rest);Edify
I
11

edit: The most refined version can be found here

Here's the most robust solution I've found thus far:

// prettier-ignore
type Exact<A, B> = (<T>() => T extends A ? 1 : 0) extends (<T>() => T extends B ? 1 : 0)
    ? (A extends B ? (B extends A ? unknown : never) : never)
    : never

/** Fails when `actual` and `expected` have different types. */
declare const exactType: <Actual, Expected>(
    actual: Actual & Exact<Actual, Expected>,
    expected: Expected & Exact<Actual, Expected>
) => Expected

Thanks to @jcalz for pointing in the right direction!

Inmesh answered 12/1, 2019 at 14:50 Comment(1)
fails for Exact<1, 1>Arsyvarsy
C
9

I wrote a library, tsafe, that lets you do that.

enter image description here

Thank @jcalz, your answer helped a lot in making this possible!

Coalfield answered 29/9, 2021 at 14:59 Comment(1)
A
4

I was a bit annoyed that the other propositions imply that I only get false without any detail to understand why it is failing.

This is how I solved it for my use case (and it gives readable errors):

type X = { color: string };
type Y = { color: string };
type Z = { color: number };

const assert = <A, B extends A, C extends B>() => {}

/** Pass! */
assert<X, Y, X>(); 

/**
 * Fail nicely:
 * Type 'Z' does not satisfy the constraint 'X'.
 * Types of property 'color' are incompatible.
 * Type 'number' is not assignable to type 'string'.
 */
assert<X, Z, X>(); 

Accelerant answered 2/10, 2021 at 0:53 Comment(0)
A
2

The most robust Equals that I've seen so far (though still not perfect) is this one:

type Equals<A, B> = _HalfEquals<A, B> extends true ? _HalfEquals<B, A> : false;

type _HalfEquals<A, B> = (
    A extends unknown
        ? (
              B extends unknown
                  ? A extends B
                      ? B extends A
                          ? keyof A extends keyof B
                              ? keyof B extends keyof A
                                  ? A extends object
                                      ? _DeepHalfEquals<A, B, keyof A> extends true
                                          ? 1
                                          : never
                                      : 1
                                  : never
                              : never
                          : never
                      : never
                  : unknown
          ) extends never
            ? 0
            : never
        : unknown
) extends never
    ? true
    : false;

type _DeepHalfEquals<A, B extends A, K extends keyof A> = (
    K extends unknown ? (Equals<A[K], B[K]> extends true ? never : 0) : unknown
) extends never
    ? true
    : false;

It fails for Equals<[any, number], [number, any]>, for example.

found here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-845655557

Arsyvarsy answered 23/5, 2021 at 9:15 Comment(0)
Y
-4

We should take different approaches depending on the problem. For example, if we know that we're comparing numbers with any, we can use typeof().

If we're comparing interfaces, for example, we can use this approach:

function instanceOfA(object: any): object is A {
    return 'member' in object;
}
Yardman answered 17/12, 2018 at 0:12 Comment(1)
Sorry, these types are not meant for code that will be executed. It's just for testing ambient type definitions. At least my use case is.Inmesh

© 2022 - 2024 — McMap. All rights reserved.