microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.02k stars 12.49k forks source link

Satisfies hinting for equality type overlap #51519

Open KyleDavidE opened 1 year ago

KyleDavidE commented 1 year ago

Suggestion

🔍 Search Terms

comparison, satisfies, equality, overlap, equals

✅ Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

Add the ability for satisfies to hint a possible overlapping type for equality. Eg given x: X and y: Y if X extends S and Y extends S then x satisfies S === y should type check even though x == y does not.

Right now, typescript has a check for type overlaps on equality. This catches cases where you compared the wrong values but is sometimes wrong if you have two interfaces, since classes can implement more than one interface. Often

📃 Motivating Example

interface CommonParent {
  buz(): void;
}

interface Foo extends CommonParent {
  foo(): void;
}

interface Bar extends CommonParent {
  bar(): void;
}

class Both implements Foo, Bar {
  buz(): void {}
  bar(): void {}
  foo(): void {}
}

function compare(l: Foo, r: Bar): boolean {
  return l satisfies CommonParent == r;
}

💻 Use Cases

It's a bit hard to come up with a compact real-world example of this coming up because it often happens as a consequence of the structure of large codebases. I have mostly seen it come up when comparing an abstract class implementation of an interface with an instance of a more-specific interface, eg:

interface IThing {
  thingStuff(): void;
}

abstract class AThing implements IThing {
  thingStuff() {}
  abstract method(): void;
}

interface IMoreSpecificThing extends IThing {
  otherStuff(): void;
}

class MoreSpecificThingImpl extends AThing implements IMoreSpecificThing {
  method(): void {}
  otherStuff(): void {}
}

// deep inside some logic
function compare(l: AThing, r: IMoreSpecificThing): boolean {
  return l satisfies IThing == r;
}

In the meantime, using as casting as a workaround works, but in general it would be nice to avoid using type-unsafe casting in places where it isn't need. An alternative to this would be an explicit upcasting operator.

johnw42 commented 1 year ago

As far as I can tell, the issue here is that whenever T satisfies U compiles, it always means just T; any type information in U is simply discarded. It's a simple rule to understand, but I think it could be made more useful while still retaining the spirit of the original rule.

I have a similar case where I think it would be better for satisfies to be more than just an assertion. In the example below, the inferred type of foo is {a: string}, but I really want it to be {a: string: b?: string}. In other words, I want satisfies to change the type such that if b is present, its type is string.

interface Partial {
  a: string;
  b?: string;
}

function foo() {
  return [{ a: "a" }] satisfies Struct;
}

What I'm suggesting is almost like making T satisfies U implicitly mean T satisfies U as T & Partial<U>, but it breaks when the interesting type is nested in another type like an array: T[] satisfies U[] as T[] & Partial<U[]> /*WRONG*/; it should be smart enough to apply Partial recursively, like this: T[] satisfies U[] as T[] & Partial<Partial<U>[]>.

RyanCavanaugh commented 1 year ago

@johnw42 that behavior (or something akin to it) was discussed in the original satisfies proposal and rejected due to other bad side effects

I don't understand why x as unknown === y isn't a good solution tbh