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
Comments
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 |
@AlCalzone example: type X=Equals<{x:any},{x:number}>;//true |
|
Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after 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 |
Thank you |
It work, but how? |
@jituanlin AFAIK it relies on conditional types being deferred when
|
@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 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 |
Can we re-open this or something? Because there isn't a way =( |
Seems like it's fixed in So, this doesn't need to be re-opened, I suppose. I forgot to link to that comment sooner, my bad. |
where can I find the infomations about the internal 'isTypeIdenticalTo' check? I can't find anything in the typescript official website.... |
@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). |
not work for:
|
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. |
@wvanvugt-speedline 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>>> |
I found this in
But I'm still not very clear what the related mean here? I can't understand the src code of |
I write some simple tests, seems 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 |
@johncmunson it is to stop them from distributing without wrapping them with [], you may get 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 |
@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. |
no, this is a partial example, it's mandatory to check union equality. But it's still not good enough. This comparison will fail to compare 'any'. export type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false; |
========
however turning to array or tuple is preferrable because they do not transform the type to something else, for example in above code bascially T[] or [T] will always be T[] or [T], unlike T & {} that may or may not return T ================= 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 this is because a bare and 0 type is ================= |
…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.
…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.
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; |
@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 }> |
Currently I use a brute force approach which deeply replaces 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. |
@geoffreytools i can't seem to get anything to fail in import { test } from 'ts-spec';
test('test description' as const, t =>
t.equal(1)<string>() // no error
); |
@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. |
`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.
Search Terms
Suggestion
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
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: