Skip to content

keyof inconsistent between primitive types #56876

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
rotu opened this issue Dec 26, 2023 · 7 comments
Closed

keyof inconsistent between primitive types #56876

rotu opened this issue Dec 26, 2023 · 7 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@rotu
Copy link

rotu commented Dec 26, 2023

πŸ”Ž Search Terms

keyof, primitive

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about keyof

⏯ Playground Link

https://www.typescriptlang.org/play?target=9&jsx=0&ts=5.4.0-dev.20231225#code/PTAEBcAsFMGdtAYwIb1qAZsglgG1NgHagAqAngA7QDKiATtheKAKwB0ALGwAwC0AJtABuAKBGIA9oVjNkAeQBGAK2iJmAXlABvAB4AuAIwBfEQHJwE6uAaEA5qdCxk4bLAzY4oANbQyEjBCU0P6g8sqq4GKS0rIAYgCuhGrYUqCaABQAlOoAfFom5pbWRPaOzq7unj5+AeBBIcgJSS5SUVIyoQBCEhL4mtbx0GYWVjalTi5uHujVIXVUDd29YiAQMJi9uBIA7iVIqJ7IuPiw8YiI0ND8bTGhAIJ0dMhkaaAA2oTxxwC6w0VjDgmFWm3l8c3qAWQDyeZBuHWQoz2mgARMi-oi7IDylMqmDahDQhjbHDZAA5eIAWwU0DorwM6OKmLKk0qMzxgQWkPJVJpJK62FsAElCBpQBwAEyEBkA5nA3E1DnBSGdAXCyLidqyahkKm9V7a3W4dIcTLSkpYlkg2b4zmEnUKXpAA

πŸ’» Code

// these cases fail in TypeScript 5.4.0-dev

const aObject = {x:1}
'toString' satisfies keyof typeof aObject

const aFunction = ()=>{}
'toString' satisfies keyof typeof aFunction

const aBool = true
'toString' satisfies keyof typeof aBool

// the following cases all succeed

const aArray = [null]
'toString' satisfies keyof typeof aArray

const aString = ""
'toString' satisfies keyof typeof aString

const aNumber = 1
'toString' satisfies keyof typeof aNumber

const aBigInt = 42n
'toString' satisfies keyof typeof aBigInt

const aSymbol = Symbol(4)
'toString' satisfies keyof typeof aSymbol

πŸ™ Actual behavior

toString is in the key type of arrays, strings, numbers, bigints, and symbols
toString is NOT in the key type of objects, functions, and bools

πŸ™‚ Expected behavior

I expect keyof to either always include toString or never include toString (not sure which).

Additional information about the issue

No response

@jcalz
Copy link
Contributor

jcalz commented Dec 26, 2023

Effectively same issue as #30225. toString exists implicitly on all nonnullish things, but only appears explicitly as a key in certain interfaces. This is not considered a bug.

@rotu
Copy link
Author

rotu commented Dec 26, 2023

Effectively same issue as #30225. toString exists implicitly on all nonnullish things, but only appears explicitly as a key in certain interfaces. This is not considered a bug.

  1. Yes, as a member of Object.prototype, toString exists by default on all non-null objects. It does exist as a specific member of interface Object and interface Function, but not interface Boolean.
  2. That can’t be the whole story. aObject satisfies Object and ’toString’ satisfies keyof Object. It seems like toString satisfies keyof typeof aObject should follow from this alone, no?

@rotu
Copy link
Author

rotu commented Dec 26, 2023

Ah, looking at #30225 again I see what you mean. {x:1} is an instance of the Object interface not because it satisfies the interface but because TypeScript incompletely infers the type as {x:1} and does not check for conformance with the Object interface (WTF).

To get keyof to accurately reflect the keys, I'd have to do something like this:

const aObject = {x:1}
const aObjectAsObject = aObject as (typeof aObject & Object)
'toString' satisfies keyof typeof aObjectAsObject

Do you know if there's any way to have TypeScript respect Object.prototype and Function.prototype instead of completely ignoring them? Perhaps a different lib?

@fatcerberus
Copy link

fatcerberus commented Dec 26, 2023

{x:1} is an instance of the Object interface not because it satisfies the interface but because TypeScript incompletely infers the type as {x:1} and does not check for conformance with the Object interface (WTF).

That's all 100% intentional:
#30225 (comment)

Apparent properties (such as toString) intentionally aren't included in keyof or inheritance validity checks, so some inconsistencies between keyof and property access is expected and intentional.

It's also worth noting that { x: 1 } is not an incorrect inference, because types aren't exact, and furthermore TS assumes that all objects will have Object.prototype in their prototype chain at runtime (usually a safe bet, except for the edge case of Object.create(null)).

@rotu
Copy link
Author

rotu commented Dec 26, 2023

That's all 100% intentional: #30225 (comment)

Apparent properties (such as toString) intentionally aren't included in keyof or inheritance validity checks, so some inconsistencies between keyof and property access is expected and intentional.

That makes sense, and it appears this is at the very least an instance of under-documentation of what keyof means on this page and that certain properties (I don't know what "apparent property" means) are ignored for type compatibility on this page. Or maybe there's some more in-depth documentation I missed?

Edit: I found a reference in this comment to "apparent type" and a mention in this deprecated handbook but nothing current.

It's also worth noting that { x: 1 } is not an incorrect inference

Yes, that is a correct inference, and it means "this has one typed property x with type 1".

If the type does not contain toString, then I would expect type H = (typeof aObject)['toString'] to be unknown, any, or undefined. But it's ()=>string. This is an indexed access type, not property access so should have the typing (not runtime) semantics (???)


Circling back to the original issue, how can toString be said to be a key of an Array but not a key of a Function?

@RyanCavanaugh
Copy link
Member

I don't know why we put toString on Array but not Function. It's almost certainly been that way since 0.8, though. Someone could try removing it, I guess?

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Jan 2, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants