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

type parameter with a constraint on an indexed type cannot be indexed #31726

Closed
sthuck opened this issue Jun 2, 2019 · 6 comments
Closed

type parameter with a constraint on an indexed type cannot be indexed #31726

sthuck opened this issue Jun 2, 2019 · 6 comments

Comments

@sthuck
Copy link

sthuck commented Jun 2, 2019

TypeScript Version: 3.6.0-dev.20190602

Search Terms:
generics, indexed types
Code

function foo<E extends {[key: string]: string}>(e: E) {
  e['a'] = 'a';
}

Expected behavior:
you should be able to index the e function parameter, as was the behavior before 3.5.
Actual behavior:
in typescript 3.5 this code results in a Type '"a"' cannot be used to index type 'E' error.

I understand this is (probably) caused by this change #30637

But since I'm adding a constraint on the type parameter I feel like this still keeps type safety and should be a bug.

(This feels like such an obvious use case I figured there has to be an issue already open or closed and I was really hesitant on opening this issue, but I couldn't find any. If it's a duplicate, sorry...)

@fatcerberus
Copy link

fatcerberus commented Jun 2, 2019

Hmm, this is an interesting case. Subtyping (extends) is essentially a question of substitutability. Structural typing requires that a subtype have at least the properties of the base type, so the only things substitutable in place of { [key: string]: string } would necessarily also have a [key: string] index signature. So I would consider the error Type '"a"' cannot be used to index type 'E' to be a bug.

The interesting thing is that the assignment is unsound: We don't know that E doesn't have a more specific type for its index signature than string. [key: string]: "foo" is perfectly fine to use in place of [key: string]: string. But that's definitely not the issue here, since TS doesn't typically enforce contravariance anyway:

function foo<E extends { pig: string }>(e: E) {
	e.pig = "oh no";  // this checks out
}

let x = { pig: "it eats people" } as const;
foo(x);

@ajafff
Copy link
Contributor

ajafff commented Jun 2, 2019

This is working as intended. This behavior was introduced by #30769

@fatcerberus
Copy link

Hmm, #30769 was a broader change than I realized. Is there a case where the code above (or something like it) wouldn’t be typesafe? You can’t remove properties from a subtype (otherwise it’s not a subtype anymore), so AFAICT E extends { [x: string]: string } could only be a type with a similar catch-all signature.

@ajafff
Copy link
Contributor

ajafff commented Jun 2, 2019

AFAICT E extends { [x: string]: string } could only be a type with a similar catch-all signature.

That's not the case. It only enforces that all known properties (if any) of the parameter are of type string. The following calls are valid using the declaration of the OP:

foo({});
foo({x: ''});

@fatcerberus
Copy link

fatcerberus commented Jun 2, 2019

Hmm, but you can’t actually index the type with arbitrary keys then, can you? So the function would also have to accept one or more keyof E to be useful, I’m thinking.

Thanks for the explanation!

@sthuck
Copy link
Author

sthuck commented Jun 2, 2019

thank you guys for helping and referring me to the original PR, this issue #31661 seems to be the same and has some more discussion.

@sthuck sthuck closed this as completed Jun 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants