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

Issue deriving tuple types on overridden method #57387

Closed
pvande opened this issue Feb 12, 2024 · 10 comments
Closed

Issue deriving tuple types on overridden method #57387

pvande opened this issue Feb 12, 2024 · 10 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@pvande
Copy link

pvande commented Feb 12, 2024

πŸ”Ž Search Terms

tuple type infer inference inheritance

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about tuples and type inference.

⏯ Playground Link

https://www.typescriptlang.org/play?alwaysStrict=false&target=2&jsx=0&module=5&ts=5.3.3&filetype=ts#code/CYUwxgNghgTiAEAXAngBwQZUTAlgOwHMAxHGAZ0QBUBXVCBAXngG0LdCAaeAOl6j2QBdANwAoUZChky8IgHs58AN6j4a+GXRgAFAEoAXPCztipCjToIV6m-DiJqMPCwBEACxAQIcl4NXqAX1EgiWhpeAAhWHgQAA9EEDxgGXlFa3VNcD1lf1s7EAcnVwIFYAAjZBAXLiUyOQBbArd8AkMXTzIqgL8bIICgA

πŸ’» Code

declare type StringFirstTuple = [string, ...any];

class Foo {
    spec(): StringFirstTuple {
        return ["hello"]
    }
}

class Bar extends Foo {
    spec() {
        return ["goodbye", {something: "else"}]
    }
}

πŸ™ Actual behavior

The spec method on Bar failed to typecheck, despite unambiguously implementing the same type signature as spec on Foo.

Annotating Bar's spec method to return a StringFirstTuple typechecks properly.

The error message seems to indicate that the return type of spec in Bar has been generalized to Array<string | {something: string}> before checking the overridden type.

πŸ™‚ Expected behavior

A function that returns an array that is hard-coded to match a tuple type should be substitutable for a function that is specified to return that tuple type.

Additional information about the issue

No response

@MartinJohns
Copy link
Contributor

MartinJohns commented Feb 12, 2024

Methods don't inherit the types of the methods they're overriding, so the type of your Bar.spec method is inferred from the return value. #32082 matches this the most.

@pvande
Copy link
Author

pvande commented Feb 12, 2024

I suppose that's fair, though from an ergonomics perspective it feels odd to me that TS would mark a type mismatch between Bar.spec and Foo.spec, but never actually check whether Bar.spec and Foo.spec could be described equivalently.

In a TypeScript-forward project, where relying on implicit types is generally discouraged, the current behavior may be completely reasonable; I'm running into this in a Javascript project with JSDoc types, and the lack of inference is an unfortunate hurdle for most consumers (especially as the tuple type gets pushed deeper inside a nested object).

@RyanCavanaugh
Copy link
Member

You can't really remove this inconsistency, only change where it appears. If you had written this, for example, m is unambiguously inferred as string[] and the code still fails

class Bar extends Foo {
    spec() {
        const m = ["goodbye", {something: "else"}]
        return m;
    }
}

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Feb 12, 2024
@pvande
Copy link
Author

pvande commented Feb 12, 2024

@RyanCavanaugh I'm not sure I follow your argument. In your example, m will always be inferred as Array<string | {something: string}> (unless I'm really missing something), and no, neither string[] nor Array<string | {something: string}> will satisfy [string, ...any].

On the other hand, [string, ...any] describes an array of any length which has a string at index 0, which absolutely can be satisfied by a (edit: non-empty) string[]. And while it's similarly correct to say that ["goodbye", {something: "else}] is an Array<string | {something: string}>, it is also a [string, ...any] (and a [string, object], and many other things).

To someone unfamiliar with the implementation of TypeScript, it definitely feels like a defect to say that a hard-coded value like ["goodbye"] is not a satisfactory "array that begins with a string" unless it is also explicitly described as such.

@MartinJohns
Copy link
Contributor

On the other hand, [string, ...any] describes an array of any length which has a string at index 0, which absolutely can be satisfied by a string[].

Arrays can be empty, not having a string at index 0.

@pvande
Copy link
Author

pvande commented Feb 12, 2024

On the other hand, [string, ...any] describes an array of any length which has a string at index 0, which absolutely can be satisfied by a string[].

Arrays can be empty, not having a string at index 0.

Fair point.

@RyanCavanaugh
Copy link
Member

@pvande the vibes might be off, but the "fix" is that the method return type inference algorithm gets more complex (and very much more non-local in this case), and "the type system is too complicated to understand" is already a problem that people talk about. Everything can be made more complicated, but complexity itself is also understood to be a defect. It's not infrequent for people to claim that the inference system should be "worse" (less likely to understand implicit intent) for the sake of being "simpler". I'm not saying this is an absolute must-not-change, but it's also not a bug in the sense of producing unsound behavior. And even in the presence of a fix, m is also "wrong", so it's not like a fix here relieves you of the burden of understanding the concept that the inferred type of an expression might be wider than its tightest possible bound.

@pvande
Copy link
Author

pvande commented Feb 13, 2024

@RyanCavanaugh Thanks for your patience with me about this.

… the "fix" is that the method return type inference algorithm gets more complex…

That does seem likely.

It's not infrequent for people to claim that the inference system should be "worse" (less likely to understand implicit intent) for the sake of being "simpler".

Not being a regular member in this community, that's not something I've been aware of, though it also seems reasonable.

…it's also not a bug in the sense of producing unsound behavior.

Completely agree. At worst, it's a spurious error in VS Code for folks leveraging a typed API in a typeless Javascript project.

And even in the presence of a fix, m is also "wrong", so it's not like a fix here relieves you of the burden of understanding the concept that the inferred type of an expression might be wider than its tightest possible bound.

I can imagine a world in which implicitly typed literals are lazily evaluated and "cast" at point-of-use. In such a world, all of the following seem reasonable:

const value = ["string", {number: 2}, /s/]

let a: [string, ...any] = value
let b: [any, {number: number}, any] = value
let c: [...any, RegExp] = value
let d: Array<string | {number: number} | RegExp> = value

function e(arg: [string, ...any]): void { arg }
e(value)

const f = () => value
a = f()
b = f()
c = f()
d = f()

What I don't know offhand is what the practicalities and drawbacks of such an implementation are, though I'm confident they exist.

That said, my only goal in opening this issue was to call attention to a sharp edge that my team encountered, to raise awareness and discussion.

@fatcerberus
Copy link

fatcerberus commented Feb 14, 2024

I can imagine a world in which implicitly typed literals are lazily evaluated and "cast" at point-of-use. In such a world, all of the following seem reasonable:

This is an interesting thought and not something I personally ever considered. One of the fundamental tradeoffs you make with a typed language is that you give up the ability to speak of individual values in favor of giving the compiler (and potentially, the programmer) an easier job by being able to reason about more general types (i.e. constraints) of values instead. But I could definitely imagine a world too where e.g. object and array literals (which might satisfy several different constraints thanks to contextual typing) have type inference "deferred" until they're actually assigned to something with an explicit type annotation.

That said, deferral brings with it its own problems. See, e.g. conditional types in a generic context.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants