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

keyof on types originating from javascript object literals is (nearly) useless #25987

Closed
weswigham opened this issue Jul 26, 2018 · 12 comments
Closed
Assignees
Labels
Bug A bug in TypeScript

Comments

@weswigham
Copy link
Member

weswigham commented Jul 26, 2018

TypeScript Version: 3.1.0-dev.201xxxxx

Search Terms:

Code

In a js file:

// @ts-check
const x = {
    y: 12
};

/** @type {keyof typeof x} */
const q = "z"; // Assignment is allowed, but is not what was desired!

Expected behavior:
keyof returns the keys the object actually has or is declared with.

Actual behavior:
keyof returns string | number, thanks the the any-index signature shoved into every object literal expression's type in JS.

This came up on DefinitelyTyped - strongly typing react's propTypes is virtually impossible because all of the key filtering and mapping operations (eg, Exclude) just don't work on any JS object literals (unless all of your keys are unique symbols (nobody would do this, I don't think), since those aren't mangled by subtype reduction with the index signature).

cc @mhegazy This is a blocker in rolling out the better React inferences to our JS users.

@weswigham weswigham changed the title keyof on types originating from javascript object literals is useless keyof on types originating from javascript object literals is (nearly) useless Jul 26, 2018
@DanielRosenwasser
Copy link
Member

To be clear, this isn't blocking defaultProps, is it?

@mhegazy mhegazy added Bug A bug in TypeScript Salsa labels Jul 26, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 26, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Jul 26, 2018

had an offline discussion about this, we should do the right thing and mark the types of JS object literals with a flag to allow for looser property access checking and assignablity checking, instead of manifesting an index signature.

@weswigham
Copy link
Member Author

weswigham commented Jul 26, 2018

To be clear, this isn't blocking defaultProps, is it?

It is blocking it in JS, yes.

@ajafff
Copy link
Contributor

ajafff commented Jul 27, 2018

This is not really specific to JavaScript object literals. That's just a small part of the bigger problem. It's a limitation of keyof with index signatures. I had this problem a few days ago when I wanted to extract all known keys of ts.CompilerOptions.

I came up with a little helper type that extracts all known keys:

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U} ? U : never;

This way you don't need to change any type inference.

@weswigham
Copy link
Member Author

weswigham commented Jul 27, 2018

Using inference to rebuild the keys sans index signature. Nifty, though very difficult to look at. It can work, though I think I'd have trouble explaining it to anyone.

Still - potentially needing to perform such contortions on any type from a js object literal expression seems a bit excessive - it's certainly not what came to my mind as a solution.

@jcalz
Copy link
Contributor

jcalz commented Aug 21, 2018

@ajafff Very clever; good work.

@m93a
Copy link

m93a commented Nov 13, 2018

@ajafff Could you please help me understand the type? How exactly is it different from keyof? Is it safe to assume that it returns exactly the same thing as keyof but without index signatures? So is KnownTypes<T> & keyof T the same as KnownTypes<T>?

@ajafff
Copy link
Contributor

ajafff commented Nov 13, 2018

@m93a
The mapped type is used to create an object type where each property has a value that is exactly the key name. Index signatures have the value never. If/when TypeScript adds more index signatures (symbols for example) they need to be handled there as well.
Afterwards that mapped type is used in a conditional type which is used to infer the union of value types. Since index signatures have type never and never is assignable to anything, they are removed from the union. The result is the list of all known property names.
This should be exactly what keyof T would return if there was no index signature on T.

@ajafff
Copy link
Contributor

ajafff commented Nov 23, 2018

I just noticed a little bug in my KnownTypes definition above. It yields {} when T has no keys (e.g. empty object or primitives).

Here's the fixed version:

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U} ? ({} extends U ? never : U) : never;

@benallfree
Copy link

benallfree commented Jan 2, 2021

@ajafff Doesn't work with typed object literals, any ideas?

Playground

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U} ? ({} extends U ? never : U) : never;

type SomeType = {
   [_:string]: string
}

const foo : SomeType= {
   a: 'one',
   b: 'two',
}

type MyKeys = KnownKeys<typeof foo>  // never

@dewey92
Copy link

dewey92 commented May 28, 2021

After upgrading to TS 4.3.2, for some reason it resolves to never, so I adjusted it like so:

export type KnownKeys<T> = keyof { [K in keyof T as string extends K ? never : K]: K }

@jackieli-tes
Copy link

After upgrading to TS 4.3.2, for some reason it resolves to never, so I adjusted it like so:

export type KnownKeys<T> = keyof { [K in keyof T as string extends K ? never : K]: K }

Compatible with the original version for TS 4:

export type KnownKeys<T> = keyof {
  [K in keyof T as string extends K ? never : number extends K ? never : K]: never
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants