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

Stricter disjunction of interfaces #54051

Open
DPOH-VAR opened this issue Apr 28, 2023 · 3 comments
Open

Stricter disjunction of interfaces #54051

DPOH-VAR opened this issue Apr 28, 2023 · 3 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@DPOH-VAR
Copy link

DPOH-VAR commented Apr 28, 2023

Suggestion

🔍 Search Terms

List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.

✅ Viability Checklist

My suggestion meets these guidelines:

  • !!! This can be a breaking change
  • 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. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

📃 Motivating Example

Disjunction of interfaces can lead to an error

Here is example 1:

interface A { id: string }
interface B { id: number }

const a: A = {id: "xxxxx"};

function modifyObject(value: A | B){
    value.id = 100; // this is OK
    //    ^? (property) id: string | number
}

modifyObject(a);

console.log(a.id.toUpperCase());
//             ^? A.id: string
// [ERR]: a.id.toUpperCase is not a function

Problem: Typescript didn't find the error of type mismatch.

Example 2:

interface A { id: string }
interface B { id: number }

const v = {id: "xxxxx"} as A | B;

function modifyObject(value: A | B){
    value.id = 100; // this is OK
    //    ^? (property) id: string | number
}

modifyObject(v);

console.log(v.id.toUpperCase());
//             ^? (property) id: string | number
// TS: Property 'toUpperCase' does not exist on type 'number' --- OK!

Signature of modifyObject is same, but now code is correct.

Example 3:

interface A { id: string, readonly dataType: "A" }
interface B { id: number, readonly dataType: "B" }

const v = {id: "xxxxx", dataType: "A"} as A | B;

function modifyObject(value: A | B){
    value.id = 100; // this is NOT OK, but works
}

modifyObject(v);

console.log(v); // {id: 100, dataType: "A"} // type mismatch

It would be better if interface disjunction handled getters and setters

interface A { id: string, readonly dataType: "A" }
interface B { id: number, readonly dataType: "B" }

const v: A = {id: "xxxxx", dataType: "A"};

function modifyObject(
    value: /* automatically calculates from (A | B) */ {
        get id(): string | number;
        set id(value: string & number);
        get dataType(): "A" | "B";
    }
){
    console.log(value.id) // OK! This is number | string
    value.id = 100; // TS ERROR: 'number' is not assignable to type 'never'.
}

modifyObject(v); // OK

In this form, it will lead to breaking changes, so there can be 2 options:

  • make this behavior dependent on the flag
  • make additional type like:
function modifyObject(value: AnyOfType<A | B>){
    value.id = 100; // this is OK
    //    ^? (property) id: string | number
}

but this seems impossible

@jcalz
Copy link
Contributor

jcalz commented Apr 29, 2023

This isn't specific to unions; objects are treated as covariant in their properties even though that's unsafe when writing to them:

interface Foo {
  y: string;
}

interface Bar extends Foo {
  z: number;
}

const a = { x: { y: "abc", z: 123 } };

function modifyObject(value: { x: Foo }) {
  value.x = { y: "def" };
}

modifyObject(a);
a.x.z.toFixed() // error!

As such this is essentially covered by #9825 and specifically this comment, so we can expect this suggestion to be declined: TypeScript is intentionally unsound here.

@RyanCavanaugh
Copy link
Member

There are two separable concerns here. The first is whether properties are treated as covariant for the purposes of assignability and subtyping. The second is whether properties present in all constituents of a union appear with a different write type than read type. That second one is more addressable in my opinion because we have already done that for indexed access types. The question is just whether it's worth it or not; it doesn't really move the status quo that you need to be somewhat careful in TypeScript when doing property writes in all cases

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels May 2, 2023
@fatcerberus
Copy link

FWIW I feel like if foo["prop"] is going to have different read and write types (per #30769, yes I still remember the issue number… 😅) then that should apply equally to foo.prop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants