Skip to content

Record key type mismatch is not flagged as error #51680

Closed

Description

Bug Report

The record type does not do type validation on the Key component. A mismatched type may be assigned.

🔎 Search Terms

record, key, mismatch, type, detect, error, allowed, permitted

⏯ Playground Link

Playground link with relevant code

💻 Code

function get_value(): Record<number, string> { return {} }

function main() {
    const ghi : Record<string, string> = get_value()  // should be an error
    console.log(ghi)
}

main()

🙁 Actual behavior

Assignment to ghi from get_value is accepted as valid.

🙂 Expected behavior

Assigned to ghi should be an error as the key types do not match.

Activity

  1. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    TypeScript is structurally typed. These two types are compatible, so the assignment is fine.

    You can't represent a type that only had number-like properties in TypeScript. This would require "exact types".

  2. edaqa-uncountable commented on Nov 29, 2022

    @edaqa-uncountable
    Author

    Record takes a Key type, implying it is significant. But what you are saying is that the Key type is effectively irrelevant. If TypeScript is meant to treat these types as equivalent, then why would Key be specified in Record<Key, Value>. Are you suggesting that Key is not part of the structural type?

    This also conflicts with the ability to produce compatible types where assigned is rejected. For example, in a tagged type:

    export interface nominal<T> {
      "nominal structural brand": T
    }
    
    namespace refType {
      export enum rtFieldRefIdStr {}
    }
    
    export type FieldRefIdStr = nominal<refType.rtFieldRefIdStr> & string
    
    
    const ref : FieldRefIdStr = "example"  // this is rejected
    

    The assignment to ref is rejected as it isn't the same type, but it is definitely a compatible type if we're thinking about runtime structure.

  3. edaqa-uncountable commented on Nov 29, 2022

    @edaqa-uncountable
    Author

    This modified example from the documentation also counteracts the claim that Key is not part of the structure.

    interface CatInfo {
      age: number;
      breed: string;
    }
     
    type CatName = "miffy" | "boris" | "mordred";
     
    const cats: Record<CatName, CatInfo> = {
      miffy: { age: 10, breed: "Persian" },
      boris: { age: 5, breed: "Maine Coon" },
      "tim": { age: 16, breed: "British Shorthair" },  // creates an invalid assignment
    };
     
    cats.boris;
    

    The type of the constant data-structure is compatible with the Record type, thus the assignment should be allowed, but it is not. In this case it knows the types are not compatible.

  4. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    That's the excess property check, which is more of a linter feature than part of the type system. It only happens for object literals, and it's a common case of confusion. When the object is returned by a function it's assignable again.

  5. edaqa-uncountable commented on Nov 29, 2022

    @edaqa-uncountable
    Author

    But you can see how that check is a valuable check and would be desired for the assignment in my original example? It does not imply anything about the runtime structure; detecting it does not lie about any types.

  6. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    The assignment to ref is rejected as it isn't the same type, but it is definitely a compatible type if we're thinking about runtime structure.

    The runtime structure doesn't matter, because TypeScripts types are compile time only. The string literal is not compatible, because it's missing the property that the target type requires.

  7. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    But you can see how that check is a valuable check and would be desired for the assignment in my original example?

    Sure. That's why the issue #12936 exists. But until that feature is implemented (if ever) you have to deal with what's supported right now. The behavior you describe in your issue is not a bug.

  8. edaqa-uncountable commented on Nov 29, 2022

    @edaqa-uncountable
    Author

    I suppose I'm objecting to the fact that the Key in record is not part of its structural type. The language is effectively lying that it supports a Key parameter at all in that case.

  9. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    Again: TypeScript is structurally typed. If the structure matches, it's compatible. The language is not lying to you. If the type is not compatible anymore you get an error. For example Record<"a" | "b", number> is not assignable to Record<string, string>, because the first type says "the values of the keys a and b are of type number", whereas the latter type says "all my values no matter the key are of the type string".

    In your initial example you have a type that says "all my number-like properties must be typed string", which is compatible with a type saying "all my properties are typed string".

    This is purely a misunderstanding on your side on how the language works. It frequently happens with people more used to languages that are nominally typed.

    See also the discussion in #48906.

  10. edaqa-uncountable commented on Nov 29, 2022

    @edaqa-uncountable
    Author

    In that sense, is it valid to define a strict type as follows, since it would be structurally incompatible with any other StrictRecord:

    type StrictRecord<KeyT extends symbol|string|number, ValueT> = {
        [key in KeyT]: ValueT
    } & { "__keyType"?: undefined|KeyT }
    

    Used in place of Record in the example it produces the desired error.

    This should be structurally exclusive, since any other KeyT would produce an incompatible "__keyType" declaration.

  11. MartinJohns commented on Nov 29, 2022

    @MartinJohns
    Contributor

    That's one of the common approaches to enforce more nominal typing, yes. See also #202.

  12. fatcerberus commented on Nov 29, 2022

    @fatcerberus

    Bit of a tangent here, but this is why you should never write generics that don't use their type parameter.

    type Foo<T> = { foo: string };

    Any Foo<T> will be assignable to any other Foo<T> because it produces the same type no matter what T is. Under structural typing what matters for assignability isn't that you provided a different type parameter, but how that type parameter is used. This is why I prefer to think of generics in TypeScript as "type functions" rather than types.

  13. typescript-bot commented on Dec 2, 2022

    @typescript-bot
    Contributor

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

  14. locked as resolved and limited conversation to collaborators on Oct 22, 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions