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

Compiler reports instance is "not assignable to X" even if class is declared to implement X #45880

Closed
cakoose opened this issue Sep 14, 2021 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@cakoose
Copy link

cakoose commented Sep 14, 2021

Bug Report

🔎 Search Terms

error ignores declared interface

🕗 Version & Regression Information

Tested in the Playground and the issue occurs in 3.3.3 in the playground with 4.4.2 and nightly (4.5.0-dev20210910).

⏯ Playground Link

Link link with relevant code

💻 Code

interface Logger {
    log(level: number, message: string): void;
}

class ConsoleLogger implements Logger {
    log(message: string): void {} // Expected (Property 'log' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'.)
}

function getLogger(): Logger {
    return new ConsoleLogger(); // Unexpected (Type 'ConsoleLogger' is not assignable to type 'Logger'.)
}

🙁 Actual behavior

The TypeScript compiler reports an error in the getLogger() function.

This is not a major problem, since fixing the root cause also gets rid of the error. But it's not ideal because the second error is mostly noise.

This feels analogous to this situation (playground link):

function f(): number {
    return 'x'; // Error, as expected.
}

Math.abs(f()); // No error, as expected.

🙂 Expected behavior

I would expect there to be no error reported in the getLogger() function.

@andrewbranch andrewbranch added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 15, 2021
@andrewbranch
Copy link
Member

I can agree that this is not the best error reporting experience if you were designing it from a blank slate, but this is the only thing that makes sense given our structural type system. The whole idea of a structural type system is that you don’t get to be a Logger just by saying that you are; you have to look and act like a Logger. The type of ConsoleLogger is completely unaffected by its implements clause; like all other types in the type system; a ConsoleLogger can only be understood by the properties and methods it actually has. The implements clause simply adds an additional check that it satisfies another contract.

@andrewbranch
Copy link
Member

It does feel similar to the incorrectly-returning function example, but the return type annotation on a function is a much stronger source of intent than an implements clause. The return type annotation is the source of truth for what a function returns, and potentially many return statements inside that function will have to conform to it and will be contextually typed by it. The implements clause never works like a source of truth for properties and methods on the class—the class is free to fulfill that contract with different types than the interface it implements, as long as the class remains assignable to the interface.

@fatcerberus
Copy link

Devil’s advocate: If a type breaks the contract of its implements clause, it’s 1) not well-defined and 2) guaranteed to produce an error already, so it could make sense to suppress any errors related to assignability to/from that type as they are just noise at that point.

@jcalz
Copy link
Contributor

jcalz commented Sep 15, 2021

The type of ConsoleLogger is completely unaffected by its implements clause

[cries in #32082 and assortment of linked issues]

@cakoose
Copy link
Author

cakoose commented Sep 16, 2021

The implements clause never works like a source of truth for properties and methods on the class—the class is free to fulfill that contract with different types than the interface it implements, as long as the class remains assignable to the interface.

Isn't this still the same as in the return type analogy? The function body can return values with different types than the declared return type, as long as the values remain assignable to the return type.

@andrewbranch
Copy link
Member

No:

interface I {
  p: string;
}
class C implements I {
  p: "hello" = "hello";
}
new C().p; // "hello"



function f(): string {
  return "hello" as const;
}
f(); // string

@cakoose
Copy link
Author

cakoose commented Sep 16, 2021

@andrewbranch: Ah, that's what you mean. Makes sense.

It still feels like the desired behavior is for TS to only error at the class declaration site and not at the use site, and it doesn't seem like that's actually incompatible with a structural type system.

For example, one way to see it: the type of new ConsoleLogger() is the inferred type from the declared members & the declared implements clauses. This will have an issue with variance, but that's expected in TS, e.g.

interface I {
  p: string;
}
class C implements I {
  p: "hello" = "hello";
}
function f(i: I) {
    i.p = "bye";
}
f(new C()); // loose variance sinks ships :-)

Playground link.

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants