Skip to content

keyof { [x: string]: unknown } unexpectedly includes number #48269

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
otonashixav opened this issue Mar 15, 2022 · 5 comments
Closed

keyof { [x: string]: unknown } unexpectedly includes number #48269

otonashixav opened this issue Mar 15, 2022 · 5 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@otonashixav
Copy link

otonashixav commented Mar 15, 2022

Bug Report

Types with an index signature for strings have number included in keyof, that is that keyof { [x: string]: unknown } = string | number. I find this unexpected. A possible explanation for this behaviour is that number can index such types be being implicitly coerced to a string:

const a: { [x: string]: unknown; } = {};
a[0] = "example"; // this is fine

However, consider that Record<string, unknown> is equally capable of such behaviour, and keyof Record<string, unknown> does not include number:

const b: Record<string, unknown> = {};
b[0] = "inconsistency"; // this is also fine

In that case, does it make sense for number to appear, considering it is not a prerequisite for numbers to implicitly index string-indexed types? Or alternatively, does Record<string, unknown> incorrectly not report number?

More examples are provided in the playground link.

🔎 Search Terms

  • keyof string index signature
  • keyof index signature
  • index signature
  • string index signature includes number

🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about ctrl-f "keyof"

⏯ Playground Link

Playground link with relevant code

💻 Code

This is a short snippet of the playground example with the main inconsistent example.

type A = { [x: string]: unknown; }
type KA = keyof A;
//   ^? - type KA = string | number
const a: A = {};
a[0]; // indexable by number therefore number in keyof A?

type B = Record<string, unknown>
type KB = keyof B;
//   ^? - type KB = string
const b: B = {};
b[0]; // indexable by number without number in keyof B

🙁 Actual behavior

keyof { [x: string]: unknown } unexpectedly includes number, whereas keyof Record<string, unknown> does not, which is inconsistent.

🙂 Expected behavior

keyof { [x: string]: unknown } should not include number.

I find this more reasonable than the alternative possibly expected behaviour: keyof Record<string, unknown> and all types which number can index via being implicitly coerced to a string should include number as a key.

@MartinJohns
Copy link
Contributor

MartinJohns commented Mar 15, 2022

This is working as intended: #23592 & Release Notes 2.9

@otonashixav
Copy link
Author

otonashixav commented Mar 15, 2022

If it is working as intended, should keyof Record<string, unknown> also be string | number, seeing as numbers have no issue indexing it? Or should numbers not be allowed to index Record<string, unknown>, or otherwise is { [x in string]: unknown } somehow distinct from { [x: string]: unknown }?

I'm assuming this description of keyof is how it is intended to work in this scenario: #23994 (comment)

keyof is the set of "known" keys for a type. in other words, set of keys the compiler will let you index into the type without a no-implicit-any-error. so if a type has a number index signature, it can be only indexed with number; if it has string index signature, it can be indexed with number or with string.

@fatcerberus
Copy link

Record<string, unknown> behaves differently than the equivalent index signature because it's constructed using a mapped type. Why the behavior is different I don't know, but either way it's still legal to index the record type with a numeric key, so it seems like the keyof behavior might be itself a bug:
https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZiEAuGAlApqATgEwDzRYCWYA5gDQwCuYA1mCAO5gB8MAvDAN4C+A3AChQkWACMAhlhRcYAbQAeKQiVIBdFDXpMwMHh278BQ8NHiIA0ugCeKWtZBwYUKwAd0DsyH0AiBCAlY3oIA9MEw4TAAegD8xiIwAZY2MHZWHs5uHgH6ACwATCFhETFGfrIADKr6ABwAjAUCARVVnPXl1YJAA

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 15, 2022
@RyanCavanaugh
Copy link
Member

Keeping keyof Record<string, unknown> as string means it has proper variance behavior.

The inconsistency can't be eliminated, just moved around, since Record<"0", unknown> is not ever going to be "0" | 0

@justingrant
Copy link
Contributor

justingrant commented Apr 18, 2025

Keeping keyof Record<string, unknown> as string means it has proper variance behavior.

Leaving a breadcrumb for future readers: even though a single Record will produce only the desired behavior, if you combine two Record<string, unknown> objects using object spread then the resulting key type will be string | number.

I assume this is #56985.

const foo: Record<string, unknown> = {foo: 'foobar' };
const bar: Record<string, unknown> = {bar: 'foobar' };
const foobar = {...foo, ...bar };

const fooKey: keyof typeof foo = "whatever";
//    ^? const fooKey: string;
const barKey: keyof typeof bar = "whatever";
//    ^? const barKey: string;
const foobarKey: keyof typeof foobar = "whatever";
//    ^? const foobarKey: string | number;

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAZiEAuGAlApqATgEwDzRYCWYA5gDQwCuYA1mCAO5gB8MAvDAN4LIwDkvAEYBDLPxgBfANwAoUJFiisKDNnyESFanQbM2nLspSDEyiTPnho8M2I7cAdM96Vnj5VLlXFtkAGl0AE8UWmCQOBgoIIAHdAi-BwAiRgALESh0ADd0LCS5AHoCmBKYAD0Afh8bZUCQmDCghOi4hM9OFPTMnLzC4tLK6thhMTrQ8MiW+MiRrGS0jOzc-Nki0vKqoA

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