Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request]type level equal operator #27024

Closed
4 tasks done
kgtkr opened this issue Sep 11, 2018 · 46 comments
Closed
4 tasks done

[Feature request]type level equal operator #27024

kgtkr opened this issue Sep 11, 2018 · 46 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@kgtkr
Copy link

kgtkr commented Sep 11, 2018

Search Terms

  • Type System
  • Equal

Suggestion

T1 == T2

Use Cases

TypeScript type system is highly functional.
Type level testing is required.
However, we can not easily check type equivalence.
I want a type-level equivalence operator there.

It is difficult for users to implement any when they enter.
I implemented it, but I felt it was difficult to judge the equivalence of types including any.

Examples

type A = number == string;// false
type B = 1 == 1;// true
type C = any == 1;// false
type D = 1 | 2 == 1;// false
type E = Head<[1,2,3]> == 1;// true(see:#24897)
type F = any == never;// false
type G = [any] == [number];// false
type H = {x:1}&{y:2} == {x:1,y:2}// true
function assertType<_T extends true>(){}

assertType<Head<[1,2,3]> == 1>();
assertType<Head<[1,2,3]> == 2>();// Type Error

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@AlCalzone
Copy link
Contributor

Here's a working implementation:

/**
 * Tests if two types are equal
 */
export type Equals<T, S> =
	[T] extends [S] ? (
		[S] extends [T] ? true : false
	) : false
;

The only problem is that any is "equal to" everything, except never.

@kgtkr
Copy link
Author

kgtkr commented Sep 11, 2018

@AlCalzone
I know that.(https://github.com/kgtkr/typepark/blob/master/src/test.ts)
There is a problem of not being able to judge any.

example:

type X=Equals<{x:any},{x:number}>;//true

@DanielRosenwasser
Copy link
Member

any is not assignable to never, so you should be able to determine whether or not either side is exclusively any.

@mattmccutchen
Copy link
Contributor

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Sep 21, 2018
@kgtkr
Copy link
Author

kgtkr commented Sep 22, 2018

Thank you
There was a way

@aleclarson
Copy link

aleclarson commented Apr 11, 2019

The best solution I have to date: spec.ts

Examples

@jituanlin
Copy link

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

It work, but how?
Could you provide more explanation?
I try to explain it though by Typescript's bivariant behavior or something else.
But I failed, help, pls.

@fatcerberus
Copy link

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

@weakish
Copy link

weakish commented Dec 22, 2019

@AlCalzone It seems that function overloads do not work.

type F = (x: 0, y: null) => void
type G = (x: number, y: string) => void

type EqEq<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false

// Function type intersection is defined as overloads in TypeScript.
type OF1 = EqEq<F & G, G & F> // true
type OF3 = EqEq<{ (x: 0, y: null): void; (x: number, y: null): void }, { (x: number, y: null): void; (x: 0, y: null): void }> // true

@mattmccutchen

Function overloads works:

type EqEqEq<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type OF4 = EqEqEq<{ (x: 0, y: null): void; (x: number, y: null): void }, { (x: number, y: null): void; (x: 0, y: null): void }> // false

But function type intersection does not work:

type OF2 = EqEqEq<F & G, G & F> // true

@AnyhowStep
Copy link
Contributor

Can we re-open this or something? Because there isn't a way =(

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jun 15, 2020

#37314 (comment)

Seems like it's fixed in master, with TS 4.0 as the milestone.

So, this doesn't need to be re-opened, I suppose. I forgot to link to that comment sooner, my bad.

@ldqUndefined
Copy link

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

where can I find the infomations about the internal 'isTypeIdenticalTo' check? I can't find anything in the typescript official website....

@weakish
Copy link

weakish commented Sep 9, 2020

@ldqUndefined I do not remember it well, but you may find it in the source code (or not, since TypeScript source code changed a lot).

@gogoyqj
Copy link

gogoyqj commented Feb 13, 2021

Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)

not work for:

type ExampleV51 = Equals<1 | number & {}, number>; // supposed to be true, but false got

@wvanvugt-speedline
Copy link

wvanvugt-speedline commented May 21, 2021

I devised a type which seems to work for all scenarios, including intersection types. While it lacks the elegance of the other solutions, it appears to perform better than either based on the tests.

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;

Here is a TypeScript Playground link demonstrating the functionality on all the test cases. If anyone knows how to optimize this code without sacrificing functionality, I'd love to hear.

@JomoPipi
Copy link

JomoPipi commented May 23, 2021

@wvanvugt-speedline
Nice, but it fails with this:

assertNotType<Equals<[any, number], [number, any]>>();

Here's mine (also fails there):

type And<X,Y> = X extends true ? Y extends true ? true : false : false
type IsAny<T> = 0 extends (1 & T) ? true : false

type _Equals<X,Y> = (X extends Y ? 1 : 2) extends (Y extends X ? 1 : 3) ? true : false
type _EqualTuple<X,Y,Z> = X extends [any] ? Y extends [any] ? Equals<X[number], Y[number]> : false : Z
type Equals<X,Y> = _EqualTuple<X,Y, And<_Equals<IsAny<X>, IsAny<Y>>, _Equals<X,Y>>>

playground

@tianzhich
Copy link

tianzhich commented Jun 3, 2021

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

where can I find the infomations about the internal 'isTypeIdenticalTo' check? I can't find anything in the typescript official website....

I found this in /node_modules/typescript/lib/typescript.js, by searching isTypeIdenticalTo. There are also some comments that may help someone here:

// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.

image

But I'm still not very clear what the related mean here? I can't understand the src code of isRelatedTo.

@tianzhich
Copy link

I write some simple tests, seems A related to B means type A can extend from type B.

type Foo<X> = <T>() => T extends X ? 1 : 2

type Bar<Y> = <T>() => T extends Y ? number : number

type Related = Foo<number> extends Bar<number> ? true : false // true

type UnRelated = Bar<number> extends Foo<number> ? true : false // false

@tylim88
Copy link

tylim88 commented Oct 1, 2022

@johncmunson it is to stop them from distributing

without wrapping them with [], you may get boolean as a result instead of true or false because true | false is boolean

export type Equals<T, S> =
	T extends S ? (
		S extends T ? true : false
	) : false
;

type A = Equals<1 | 2, 1 | 2> // boolean

turn them into array type or tuple type prevent them from distributing

@johncmunson
Copy link

@tylim88 Gotcha! Okay, so it's not strictly needed in order to perform the equivalency check. It's there to fix an edge case in how the result is reported, since we happen to be using booleans to indicate success or failure.

@ecyrbe
Copy link

ecyrbe commented Oct 1, 2022

no, this is a partial example, it's mandatory to check union equality.
It's used to allow to compare unions strictly, since behind the scene typescript is using distributed conditional typing on unions :
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

But it's still not good enough. This comparison will fail to compare 'any'.
To have better comparison, you need to use :

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

@tylim88
Copy link

tylim88 commented Oct 1, 2022

@tylim88 Gotcha! Okay, so it's not strictly needed in order to perform the equivalency check. It's there to fix an edge case in how the result is reported, since we happen to be using booleans to indicate success or failure.

  1. it is mandatory because unions are not edge cases
  2. it doesn't have to be boolean, it can be arbitrary types X and Y. As long as the result return X | Y, then this check is useless.

========
another thing is it doesnt have to be array or tuple to stop the distributive behavior, as long as it is not bare parameter, then it will works, example:

type Equals<T, S> =
	T & {} extends S  ? (
		S & {} extends T ? true : false
	) : false
;

type A = Equals<1 | 2, 1 | 2> // true
//   ^?

type B = Equals<1 | 2, 1 | 3> // false
//   ^?

however turning to array or tuple is preferrable because they do not transform the type to something else, for example in above code undefiend & {} result in never

bascially T[] or [T] will always be T[] or [T], unlike T & {} that may or may not return T

=================
another issue with bare parameter is when it is never

type a = never extends true? 1: 2 // 1

type b<T> = T extends true? 1: 2

type c = b<never> // never

type d<T> = T[] extends true? 1: 2

type e = d<never> // 2

if the bare parameter is never, it will not return X or Y or even X|Y, it will return never

this is because a bare never represent 0 type and if you try to distribute 0 type, you get back 0 type (multiplying anything with 0 is 0)

and 0 type is never

=================
finally as other mentioned, this kind of check is not good enough

trevorade added a commit to mmkal/expect-type that referenced this issue Nov 3, 2022
…xclusively.

This is a breaking change as I opted to remove the types that were no longer needed. They are exported though so it's likely some people depend on them.

This took a lot of tinkering. This topic and this equality check is discussed extensively at microsoft/TypeScript#27024

The main three work-arounds this implementation added are:
1. Explicitly handling `any` separately
2. Supporting identity unions
3. Supporting identity intersections

The only known issue is this case:

```ts
  // @ts-expect-error This is the bug.
  expectTypeOf<{foo: number} & {bar: string}>().toEqualTypeOf<{foo: number; bar: string}>()
```

@shicks and I could not find a tweak to the `Equality` check to make this work.

Instead, I added a workaround in the shape of a new `.simplified` modifier that works similar to `.not`:

```ts
  // The workaround is the new optional .simplified modifier.
  expectTypeOf<{foo: number} & {bar: string}>().simplified.toEqualTypeOf<{foo: number; bar: string}>()
```

I'm not entirely sure what to do with documenting `.simplified` because it's something you should never use unless you need it. The simplify operation tends to lose information about the types being tested (e.g., functions become `{}` and classes lose their constructors). I'll definitely update this PR to reference the `.simplified` modifier but I wanted to get a review on this approach first. One option would be to keep around all the `DeepBrand` stuff and to have `.deepBranded` or something being the modifier instead. That would have the benefit of preserving all the exported types making this less of a breaking change.
trevorade added a commit to mmkal/expect-type that referenced this issue Nov 28, 2022
…xclusively.

This is a breaking change as I opted to remove the types that were no longer needed. They are exported though so it's likely some people depend on them.

This took a lot of tinkering. This topic and this equality check is discussed extensively at microsoft/TypeScript#27024

The main three work-arounds this implementation added are:
1. Explicitly handling `any` separately
2. Supporting identity unions
3. Supporting identity intersections

The only known issue is this case:

```ts
  // @ts-expect-error This is the bug.
  expectTypeOf<{foo: number} & {bar: string}>().toEqualTypeOf<{foo: number; bar: string}>()
```

@shicks and I could not find a tweak to the `Equality` check to make this work.

Instead, I added a workaround in the shape of a new `.simplified` modifier that works similar to `.not`:

```ts
  // The workaround is the new optional .simplified modifier.
  expectTypeOf<{foo: number} & {bar: string}>().simplified.toEqualTypeOf<{foo: number; bar: string}>()
```

I'm not entirely sure what to do with documenting `.simplified` because it's something you should never use unless you need it. The simplify operation tends to lose information about the types being tested (e.g., functions become `{}` and classes lose their constructors). I'll definitely update this PR to reference the `.simplified` modifier but I wanted to get a review on this approach first. One option would be to keep around all the `DeepBrand` stuff and to have `.deepBranded` or something being the modifier instead. That would have the benefit of preserving all the exported types making this less of a breaking change.
@aurospire
Copy link

aurospire commented Nov 29, 2022

class Hidden { }

export type Matches<T, S> =
    [T] extends [S] ?
    [S] extends [T] ?
    [Hidden] extends [T] ?
    [Hidden] extends [S] ? true :  // For <any, any>
    false :                        // For <any, S>
    [Hidden] extends [S] ? false : // For <T, any>
    true :                         // For <T === S>
    false :                        // For <T sub S>
    false                          // For <T !== S> or <S sub T>

By using a hidden class and comparing T and S to it, this seems to prevent the any issue, as well as the intersection type issue.

const t0: Matches<1, 1> = true;
const t1: Matches<1, 2> = false;

const t2: Matches<1, 1 | 2> = false;
const t3: Matches<1 | 2, 1> = false;

const t4: Matches<{ a: true; } & { b: false; }, { a: true, b: false; }> = true;
const t5: Matches<{ a: true, b: false; }, { a: true; } & { b: false; }> = true;

const t6: Matches<number, any> = false;
const t7: Matches<any, number> = false;
const t8: Matches<any, any> = true;

const t9: Matches<never, any> = false;
const tA: Matches<any, never> = false;
const tB: Matches<never, never> = true;

@geoffreytools
Copy link

@aurospire, it fails in the following cases, but I'm curious about how it's supposed to work.

type A = Matches<[any, number], [number, any]>

type B = Matches<{ x: any }, { x: 1 }>

type C = Matches<Foo<number>, Foo<any>>
class Foo<T> { constructor(private foo: T) {} }

type D = Matches<number & { _tag: 'dollar' }, number & { _tag: any }>

@geoffreytools
Copy link

geoffreytools commented Nov 29, 2022

Currently I use a brute force approach which deeply replaces any, unknown and never by unique symbols in the types under test. It depends on module augmentation and higher kinded types to replace user-defined classes. I described how it works in the documentation of ts-spec.

It passes the test cases I found here and I'm happy with it for now but I don't believe it is full proof because there is no reason to expect it works for cases it doesn't explicitly handle, for example I didn't feel the need to test equality for overloaded functions with generics and given how hard it is to infer overloads (especially with the same arity) I imagine this could even be a limitation.

The implementation is not very complicated but it has a dependency on free-types so it would make little sense to post it here.

I don't know if it's the best tool for the job as of today, but even if it is it requires maintenance and it's not the equality operator we dream of.

@DetachHead
Copy link
Contributor

@geoffreytools i can't seem to get anything to fail in ts-spec

import { test } from 'ts-spec';

test('test description' as const, t => 
    t.equal(1)<string>() // no error
);

playground

@geoffreytools
Copy link

@DetachHead, I opened an issue if you want to continue the discussion there.

The same code works in a CodeSandbox or in a local project.

FukudaYoshiro added a commit to FukudaYoshiro/superstruct that referenced this issue Mar 23, 2023
`boolean` is effectively the union type `true | false` but the current
`isMatch` implementation will treat overlapping union types as matching.

This patch introduces a more strict type equivalence check based on
microsoft/TypeScript#27024 (comment)
and uses it when checking types that extend `boolean` for equivalence to
`boolean`.

The test added in this patch fails without the corresponding code changes
here.

Fixes #754.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests