Skip to content

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

Closed

Description

@sthuck

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...)

Activity

  1. fatcerberus commented on Jun 2, 2019

    @fatcerberus

    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);
  2. ajafff commented on Jun 2, 2019

    @ajafff
    Contributor

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

  3. fatcerberus commented on Jun 2, 2019

    @fatcerberus

    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.

  4. ajafff commented on Jun 2, 2019

    @ajafff
    Contributor

    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: ''});
  5. fatcerberus commented on Jun 2, 2019

    @fatcerberus

    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!

  6. sthuck commented on Jun 2, 2019

    @sthuck
    Author

    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.

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions