TypeScript
TypeScript copied to clipboard
[Feature request]type level equal operator
original issue: #27024
Search Terms
- Type System
- Equal
Suggestion
T1 == T2Use 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}// truefunction assertType<_T extends true>(){} assertType<Head<[1,2,3]> == 1>(); assertType<Head<[1,2,3]> == 2>();// Type ErrorChecklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. new expression-level syntax)
workarounds
to summarize the discussion in #27024, the accepted solution was:
export type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
however there are many edge cases where it fails:
- doesn't work properly with overloads (function type intersections) - https://github.com/microsoft/TypeScript/issues/27024#issuecomment-568233987
- doesn't work properly with
{}types - https://github.com/microsoft/TypeScript/issues/27024#issuecomment-778623742 - doesn't work properly with
Equal<{ x: 1 } & { y: 2 }, { x: 1, y: 2 }>- https://github.com/microsoft/TypeScript/issues/27024#issuecomment-907601804
there were some other workarounds posted that attempted to address these problems, but they also had cases where they didn't work properly.
what "equal" means
i think it's important for it to treat structurally equal types as equal. for example { a: string, b: number; } should be considered equal to { a: string } & { b: number; } as they behave exactly the same
What I see in that thread is that people have differing definitions of "equal" that are possibly in conflict, e.g. https://github.com/microsoft/TypeScript/issues/27024#issuecomment-1044193168. If you want to reopen this please fill out something more substantial in the template that goes into the shortcomings, current trade-offs, and desired semantics.
@RyanCavanaugh i don't think there was any disagreement on the definition of "equal" but rather people finding edge cases that the workarounds didn't account for. as far as i can tell everyone in that discussion seems to agree on what types should be considered equal.
regardless, i've updated the issue with more information. can it be re-opened (or can the original issue be re-opened)?
{ p: string | number; } should be considered equal to { p: string } | { p: number; }
👀
These types are very much different; T extends { p: string } ? true : false is true | false for one and false for the other
oops, updated with a better example
// for example, below also is true
type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>
// for example, below also is true, incorrect type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>
So, this solution is not quite right.
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
// for example, below also is true, incorrect type M = Equal<{ x: 1 } & { y: 2 }, { x: 3, y: 2 }>So, this solution is not quite right.
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
if you want to handle this case. you can use this :
type Simplify<T> = T extends unknown ? { [K in keyof T]: T[K] } : T;
type Equal<X, Y> =
(<T>() => T extends Simplify<X> ? 1 : 2) extends
(<T>() => T extends Simplify<Y> ? 1 : 2) ? true : false;
type M = Equal<{ x: 3 } & { y: 2 }, { x: 3, y: 2 }>
Playground Link
unfortunately that approach still isn't foolproof. it doesn't recursively fix the types:
type M = Equal<{a: { x: 3 } & { y: 2 }}, {a: { x: 3, y: 2 }}> //false
If you want , you can... Just do a recursive simplify
type Simplify<T> = T extends Record<any,unknown> ?
{ [K in keyof T]: Simplify<T[K]> } : T;
type Equal<X, Y> =
(<T>() => T extends Simplify<X> ? 1 : 2) extends
(<T>() => T extends Simplify<Y> ? 1 : 2) ? true : false;
type M = Equal<{a: { x: 3 } & { y: 2 }}, {a:{ x: 3, y: 2 }}>;
for a complete handling of all simplify use cases, check here this playground Link
i jumped down that rabbit hole as well, but i kept finding cases where my solution didn't work. same story with yours, however admittedly it's much cleaner than what i came up with.
// should be false:
const false1: Equal<(() => 2), ((value: number) => 1) & (() => 2)> = false // error
const false2: Equal<[any, number], [number, any]> = false // error
const false3: Equal<<T>(value: T) => T, (value: unknown) => unknown> = false // error
// should be true:
const true1: Equal<(() => 2) & ((value: number) => 1), ((value: number) => 1) & (() => 2)> = true // error
const true2: Equal<number & {}, number> = true // error
we could probably keep adding complexity to the Simplify type until these tests pass too (except maybe the generic one, i have no clue how to fix that), but i think it's an uphill battle which is why i think this needs to be supported in the language
I understand your point of view. I agree that the false2 should be true. so i corrected it.
I also fixed the function use case with FnA | FnB not equal to FnB.
You can check it Here
But i did it only for sports. In pratice i would not use this monster.
I think doing those in pratice, could really have a bad impact on typescript performance and could maybe be also really complex to implement for typescript team for a really small audience, and that maybe not worth it ?
As much as I would like to have a reliable equality check, I have to agree with Ryan about people disagreeing on what should be equal.
For instance, I don't think intersection of functions should be equal, because this operation is not commutative:
type A = ((a: number) => string) & ((a: 1|2|3) => 'Maria')
type B = ((a: 1|2|3) => 'Maria') & ((a: number) => string)
declare const fooA:A;
declare const fooB:B;
const barA = fooA(1) // string
const barB = fooB(1) // "Maria"
If you want an equal operator in order to catch bugs in your types, you will probably want Equal<A, B> to be false. If you want it for some other reason, maybe your definition of "equality" in that specific context will be different.
Concerning performance, if this kind of check only appears in a test suite, of course you want the said test suite to run fast, but it is not as critical as making the type checking of your production code fast, so I would not mind a slow workaround if it were reliable.
I have learned to use conditional types in times when using an intersection would have made logical sense, because intersections have lots of quirks. What would "equality" mean in an unsound type system?
interesting, i wasn't aware that it resolves to the first matching overload instead of the best one.
in that case, i agree that those two types should not be considered equal, according to my definition of "equal" in the OP that the types must "behave exactly the same"
This issue was really hard to follow, since half the discussion is about what type equality means and half is about whether it's possible to work around this gap using existing features. I think the original request for shortcomings, tradeoffs, and semantics is the current blocker.
Doesn't Typescript already internally have a concept of type equality? I think the following definition is simple and would be consistent with other language features: two types A and B are equal if
declare let a: A;
declare let b: B;
a = b;
b = a;
doesn't produce any type checking errors.
The Typescript type checker has no issue with some of these supposedly failing cases: C, D, G. I have no thoughts on whether this is right or wrong, but it seems reasonable that if these are expected to fail then this is a Typescript bug.
@RyanCavanaugh 's test above
type J={ p: string | number; };
type J2= { p: string } | { p: number; };
fails as expected.
type K = ((a: number) => string) & ((a: 1|2|3) => 'Maria')
type K2 = ((a: 1|2|3) => 'Maria') & ((a: number) => string)
declare let fooA:K;
declare let fooB:K2;
fooA = fooB;
fooB = fooA;
passes.
That being said, I think most people would be happy if someone from the TS team would just decide a definition, regardless. As pointed out earlier, the edge cases probably don't matter for most code.
I assume the definition is the major blocker here, or else the TS team doesn't want to do this for other unstated reasons (usefulness?). I'm not sure what trade-offs/shortcomings would be relevant here. Maybe a (another?) use-case would help?
Use case
I'm designing a form widget for type T, where the caller provides metadata for each P in keyof T, the required metadata of a conditional type T[P] extends string ? ....
For string form fields, assigning the initial value of type T[P] in the metadata to the <input>'s value works fine. Reading the value from the <input> value into the constructed object of type T during form submission fails because string is a superset of T[P].
Reversing the conditional to string extends T[P] fails in the opposite order (initial value fails, constructing object succeeds).
T[P] extends string && string extends T[P] doesn't work, syntactically.
The above type Equals<A, B> = ... workarounds do not work in conditional types, I assume also for syntactic reasons.
An operator with the above definition would solve this use case (bi-directional assignment).
Trade-offs, shortcomings
I'm not sure what they'd be. The expected operator == isn't currently allowed, so this wouldn't introduce ambiguity. I can't imagine this would be harder to implement than extends.
sindresorhus/type-fest#537 solves identity intersection/equality (TS Playground):
type Equals<X, Y> =
(<T>() => T extends X & T | T ? 1 : 2) extends
(<T>() => T extends Y & T | T ? 1 : 2) ? true : false;
Equals<{a: 1} & {a: 1}, {a: 1}>
//=> true, false previously
Equals<{a: 1} | {a: 1}, {a: 1}>
//=> true, false previously
[email protected] has an implementation that support all of the cases mentioned in here and related issues.
Here are all the tests: https://github.com/unional/type-plus/blob/main/ts/equal/equal.is_equal.spec.ts
UPD @unional: new link https://github.com/unional/type-plus/blob/main/packages/type-plus/src/equal/equal.is_equal.spec.ts
Another data point: neither Matt's original solution nor type-fest's work for the following test:
type Result = Equals<
// ^? type Result = false
`${string & {tag: string}}`,
`${string & {tag: string}}`
>
As I mentioned in the original issue however, this behavior might also be a typescript regression starting around v5.1.6.
UPDATE: I fixed this bug in https://github.com/microsoft/TypeScript/pull/61113, which is now merged.
@rauschma Found an interesting test case that is broken with most (all?) current type-level equals implementations. Further demonstrating the need for official support for something like this.
https://github.com/microsoft/TypeScript/issues/61547
Thinking more about a good utility type Equal<X, Y>:
- The most intuitive definition of two types being equal is them being mutually assignable.
- Additionally,
anycan only be equal to itself – otherwiseEqualcan’t really be used for testing.
Implementing that is relatively straightforward (vs. hacks requiring you to know the internals of tsc):
type Equal<X, Y> =
[IsAny<X>, IsAny<Y>] extends [true, true] ? true
: [IsAny<X>, IsAny<Y>] extends [false, false] ? MutuallyAssignable<X, Y>
: false
;
type IsAny<T> = 0 extends (1 & T) ? true : false;
type MutuallyAssignable<X, Y> =
[X] extends [Y]
? ([Y] extends [X] ? true : false)
: false
;
- More information: https://github.com/rauschma/asserttt
Equalhandles all tricky edge cases (that I’m aware of) correctly: https://github.com/rauschma/asserttt/blob/main/test/equal_test.ts
@rauschma @louwers nice find! I had searched for a case where the equals hack was actually less strict than mutual assignability, but hadn't been able to find one until now.
This modification should fix it though:
type MaxInvariance<X> = <T>() => T extends X ? (_: X) => X : 0
(at least with strictFunctionTypes enabled. Without strictFunctionTypes, I think you'd need to do two checks.)
Aside: the bug I mentioned above is now fixed.
Thank you for informative discussion.
To support composite any (another corner case), I modified @rauschma 's code;
type Equal<X, Y> = MutuallyAssignable<X, Y> extends true
? (MutuallyAssignable<DeepAny<X>, DeepAny<Y>>)
: false;
type IsAny<T> = 0 extends (1 & T) ? true : false;
type DeepAny<T> = T extends object ? { [K in keyof T]: DeepAny<T[K]> }
: IsAny<T>;
type MutuallyAssignable<X, Y> = [X] extends [Y]
? ([Y] extends [X] ? true : false)
: false;
Equal<{ x: number }, { x: any }> // -> false (previously true)
cf. TS Playground
Good observation, @ymd-h! I hadn’t thought of any inside objects and tuples.
I see one issue with DeepAny:
T extends objectshould be replaced with[T] extends [object]so that no distributivity happens.
It may be possible to simplify your code even further (playground):
type Equal<X, Y> = MutuallyAssignable<ReplaceAny<X>, ReplaceAny<Y>>;
const ANY = Symbol();
type ReplaceAny<T> = IsAny<T> extends true
? typeof ANY
: [T] extends [object]
? { [K in keyof T]: ReplaceAny<T[K]> }
: T
;
I’m not sure if my typeof ANY is the best way to obtain a unique type, though.
Edit: The following alternative has the benefit of only existing at the type level but it’s also less unique.
type AnyType = {unique: true} & 'AnyType';
It can not be solved 100% from user land. A fully working solution needs to be native. For a closer one, you can take a look at the implementation in type-plus
It can not be solved 100% from user land.
Sadly, that’s true. Example that just occurred to me:
type _ = Equal< // true; should be false
Set<number>, Set<any>
>;
The only practical & reliable solution currently is to use the equals hack, given the assumption that it is at least as strict as mutual assignability, and at most as strict as textual equality (as long as you're ok with anything between those two extremes being an implementation detail).
That assumption is/was broken in only 3 places that I'm aware of:
- Breaks textual equality upper bound: #61098 ✅ Fixed in #61113 (and now released in 5.9!)
- Breaks mutual assignability lower bound: #61547 ✅ Fixed in #61683
- Breaks textual equality upper bound: #61162 ❌ No fix identified as of yet
If the last one gets fixed, then we have everything we need for equals in user-land (and types like {a: 1} & {b: 2} can get recursively normalized to {a: 1, b: 2} in user-land if desired to cover the in-between cases, though I personally don't see why that is desirable: all my use-cases merely want to know if two types are exactly identical, i.e. as close to textually identical as is possible to determine.)
I personally doubt TypeScript will ever implement an equals operator due to the disagreement over whether {a: 1} & {b: 2} == {a: 1, b: 2}, so might as well just fix that last issue so that those of us (I suspect the majority) who don't care whether {a: 1} & {b: 2} == {a: 1, b: 2} can move on with a practical solution in place and no changes to the language's grammar.
I've been experimenting and ended up coming up with what I think is a new approach to the problem. It avoids the use of the famous trick almost entirely as it prioritizes treating types like { a: 1; b: 2 } vs { a: 1 } & { b: 2 } as equal and having a consistent notion of equality at all levels of deeply nested structures.
Unfortunately, that comes at the cost of any being treated as equal to all other types when it appears in function parameter or return value types. The reason is that there isn't a known way in Typescript to infer those types from arbitrary functions. The closest we've got is https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709, but that solution is only for non-generic functions, and sadly that tiny limitation has big consequences. If the issue gets solved one day, then full-featured intuitive user-land equality checking types will finally become possible, but for now this is the best I've got:
type IsAny<T> = 0 extends 1 & T ? true : false;
// https://stackoverflow.com/a/69850582/9861000
type IsEmptyObject<T> = [T, keyof T] extends [
Record<PropertyKey, unknown>,
never,
]
? true
: false;
type MutuallyAssignable<A, B> = [A, B] extends [B, A] ? true : false;
type Identical<A, B> =
(<T>() => T extends (A & T) | T ? 1 : 2) extends <T>() => T extends
| (B & T)
| T
? 1
: 2
? true
: false;
// For equality checks to work on recursive types, we have to keep track of
// types that have already been encountered.
type SeenList = Array<[unknown, unknown]>;
type AlreadySeen<
T extends [unknown, unknown],
Seen extends SeenList,
> = Seen extends [infer First, ...infer Rest extends Array<[unknown, unknown]>]
? Identical<T, First> extends true
? true
: AlreadySeen<T, Rest>
: false;
export type Equal<A, B, Seen extends SeenList = []> =
IsAny<A> extends true
? IsAny<B>
: IsAny<B> extends true
? false
: AlreadySeen<[A, B], Seen> extends true
? never
: MutuallyAssignable<A, B> extends true
? false extends
| EqualObjectHelper<A, B, [[A, B], ...Seen]>
| EqualEnumHelper<A, B>
? false
: true
: false;
// Note that here we for the first time force union type distribution.
type EqualObjectHelper<A, B, Seen extends SeenList> = A extends object
? B extends object
? MutuallyAssignable<A, B> extends true
? ObjectEqual<A, B, Seen>
: never
: never
: never;
type ObjectEqual<
A extends object,
B extends object,
Seen extends SeenList = [],
> =
IsEmptyObject<A> extends true // for {} vs object
? IsEmptyObject<B>
: IsEmptyObject<B> extends true
? false
: Equal<keyof A, keyof B> extends true
? false extends {
[K in keyof A & keyof B]: Equal<A[K], B[K], Seen>;
}[keyof A & keyof B]
? false
: true
: false;
// Enums are mutually assignable with number, so the check is only accurate
// if we use the Identical trick.
type EqualEnumHelper<A, B> = A extends number
? number extends A
? B extends number
? number extends B
? Identical<A, B>
: never
: never
: never
: never;
Here are some differences this solution has to the Equal type from type-plus:
type R1 = { next?: R1; a: 1 };
type R2 = { next?: R2 } & { a: 1 };
enum Enum {}
// Where my implementations is superior compared to type-plus:
type A = Equal<[{ a: 1; b: 2 }], [{ a: 1 } & { b: 2 }]>; // true
type B = Equal<{ prop: { a: 1; b: 2 } }, { prop: { a: 1 } & { b: 2 } }>; // true
type C = Equal<R1, R2>; // true
type D = Equal<Enum, number>; // false
// Where my implementations is lacking compared to type-plus:
type E = Equal<[() => string], [() => any]>; // true (should be false)
// But at least it's consistent no matter the nesting depth!
type F = Equal<() => string, () => any>; // true (same in type-plus unlike E! But should be false)
type G = Equal<Set<string>, Set<any>>; // true (should be false)
type H = Equal<() => void, (a?: number) => void>; // true (should be false)
type I = Equal<{ a: 1 }, { readonly a: 1 }>; // true (should be false?)
Finally I have to say I haven't measured the performance of my solution with complex data types, but if you want to use it in your code, it's probably something you should pay attention to. I can imagine this approach slowing things down, given its recursive nature and especially how it keeps track of already encountered types.
For me this was more of a fun project, but if you are actually going to use it in your code, I would be happy to hear how well it works for you :) If there are other edge cases I've missed, please let me know!
P.S. If you know the types you will compare don't include functions (like some JSON object schemas for example), I think this might be the best solution there is :) Also you can easily improve performance by getting rid of the seen lists if you know your types won't be recursive.
For the case where all you've got are just well-defined types for plain nested data, I've also been thinking about something like this:
type MutuallyAssignable<A, B> = [A, B] extends [B, A] ? true : false;
type Identical<A, B> =
(<T>() => T extends (A & T) | T ? 1 : 2) extends <T>() => T extends
| (B & T)
| T
? 1
: 2
? true
: false;
type DeepSimplify<T> = T extends unknown
? { [K in keyof T]: DeepSimplify<T[K]> } extends infer B
? B
: never
: never;
export type PlainEqual<A, B> =
MutuallyAssignable<A, B> extends true
? Identical<DeepSimplify<A>, DeepSimplify<B>>
: false;
DeepSimplify forces intersections to be evaluated and thus gets rid of the issue where Identical sees { a: 1; b: 2 } and { a: 1 } & { b: 2 } as different types.
But it also gets rid of really all function signatures, which means types like Set<string> and Set<number> that are normally differentiated by the types of their methods like .has() / .add() / etc. are now considered equal. To fight against this, we still do the MutuallyAssignable check at the beginning.
Is there some problem with this approach that I'm missing? It just cannot be that simple, there has to be a catch! 🤪 right?
@rauschma Found an interesting test case that is broken with most (all?) current type-level equals implementations. Further demonstrating the need for official support for something like this.
@louwers that bug is fixed in #61683, which just got merged. The only remaining equals hack bug (that I'm aware of) is #61162.