Skip to content

inconsistent behavior of interface vs type alias as a generic parameter with constraint #28174

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

Closed
tvald opened this issue Oct 26, 2018 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@tvald
Copy link

tvald commented Oct 26, 2018

TypeScript Version: 3.1.3

Search Terms:
generic parameter extends record named anonymous interface type alias constraint inconsistent different

Code

type CallbackMap = Record<string, () => any>;
type Container<X extends CallbackMap> = {};

type FooAlias = { foo(): void };
interface IFoo {
  foo(): void;
}

type ContainerOK = Container<{ foo(): void }>;
type ContainerOK2 = Container<FooAlias>;
type ContainerFail = Container<IFoo>;
// [ts] Type 'IFoo' does not satisfy the constraint 'Record<string, () => any>'.
//      Index signature is missing in type 'IFoo'.

Expected behavior:
Consistent behavior as a generic parameter regardless of whether a type is expressed as an interface, a type alias, and an anonymous (?) type. I'm not actually sure whether these are supposed to be valid or not, but I would expect them to be consistent.

Actual behavior:
The compiler only generates an error for the interface representations of the type.

Playground Link:
playground with above code

Related Issues:
none?

@tvald
Copy link
Author

tvald commented Oct 26, 2018

I'm aware that interfaces are not quite exactly the same as a type alias. I also assume that the anonymous(?) generic parameter (see ContainerOK) is converted to an anonymous type alias under the hood.

There's nothing in the documentation that indicates why this might occur, though: https://www.typescriptlang.org/docs/handbook/advanced-types.html#interfaces-vs-type-aliases

@weswigham weswigham added Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Oct 26, 2018
@ahejlsberg
Copy link
Member

The constraint Record<string, () => any> is the same as { [x: string]: () => any }, and in order to satisfy that constraint a type must have a compatible string index signature. The IFoo type doesn't, so we report an error. The { foo(): void } type doesn't either, however we implicitly provide one because we infer index signatures for object literal types (see #7029 for details).

It's somewhat unfortunate to have this subtle difference between interface and object literal types, but it is the best rule we've been able to devise to allow us to leverage knowledge that object literals have only the properties they themselves declare, thus making it safe to implicitly add an index signature.

Meanwhile, you can fix the issue using a self-referential constraint:

type Container<X extends Record<keyof X, () => any>> = {};

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Oct 27, 2018
@tvald tvald closed this as completed Oct 28, 2018
@essenmitsosse
Copy link

Thank you so much for clearifying. I was going crazy over this for quite a while now, because I couldn't figure out what was going on.

Unfortunately this a huge drawback for interfaces in my opinion. The self-referential constraint seems like a hack at best in my opinion, because its only works on generics.

It makes it basically impossible, to write a reusable (e.g. a type/interface) constrains for an interface. You would have to repeat that constrain in every place where you want to use it, which could lead to massive code duplication.

The problem is you can do this with a generic, which would allow interfaces without index signatures to be passed in:

function F<T extends { values: Record<keyof T['values'], string> }>(t: T) {};

But you can't do this (neither as a type nor as an interface):

interface I { values: Record<keyof I['values'], string> }
// ERROR: 'values' is referenced directly or indirectly in its own type annotation.

Therefor I can't use it like that (which would be great, especially in cases where I use I a lot and its quite complex

function F<T extends I>(t: T) {};

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

4 participants