Typescript: ES2019 Object.fromEntries uses PropertyKeys as mapped key type

Created on 14 May 2019  路  11Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.5.0-dev.20190514


Search Terms:
fromEntries
Code

// A *self-contained* demonstration of the problem follows...
// Test this by running `tsc` on the command-line, rather than through another build tool such as Gulp, Webpack, etc.
type T = {}

const a: { [key: string]: T } = { "key": {} }

const b: [string, T][] = Object.entries(a)

const c : {[key: number]: T; [key: string]: T } = Object.fromEntries(b)

const d : { [key: string]: T } = Object.fromEntries(b)

Expected behavior:

~Compilation should fail because number is not a key of a.~

_edit:_ The return type of Object.fromEntries(b) should be inferred as d: { [key: string]: T } .

Actual behavior:

_edit:_ The return type of Object.fromEntries(b) is {[key: number]: T; [key: string]: T } Adding an erroneous [key: number].

Object.fromEntries has type:

 fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k in PropertyKey]: T };

However if I make a second type parameter for fromEntries which extends PropertyKey and use that for the key type in forEntries:

 fromEntries<K extends PropertyKey, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T };

I'm willing to make a PR for this if this sounds like a reasonable approach.

Playground Link:

Apologies, but I could not use es2019 in the playground.

Related Issues:

https://github.com/microsoft/TypeScript/pull/30934

https://github.com/microsoft/TypeScript/issues/30933 https://github.com/microsoft/TypeScript/issues/25999

Bug Fix Available

Most helpful comment

Got the same issue randomly, not sure what triggered it as things were compiling fine. But using a target of either esnext or es2019 will cause this compilation issue. Also confirmed this by updating to typescript@next and running the same code.

Replication details are as follows, I did this in my /tmp directory and you can pretty much copy and paste these steps to reproduce.

tsconfig.json

{
  "compilerOptions": {
    "target": "es2019",
    "keyofStringsOnly": true,
  },
}

Running tsc will complain that it has nothing to do, so touch index.ts.
Then running the tsc command will result in the following error:

*chop*/typescript/lib/lib.es2019.object.d.ts:28:81 - error TS2322: Type 'string | number | symbol' is not assignable to type 'string'.
  Type 'number' is not assignable to type 'string'.

28     fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k in PropertyKey]: T };
                                                                                   ~~~~~~~~~~~

Found 1 error.

This is specifically part of es2019 (currently esnext aliases this I believe).
The fix, for now, is to "target": "es2018" instead in your tsconfig.json.

{
  "compilerOptions": {
    "target": "es2018",
    "keyofStringsOnly": true,
  },
}

All 11 comments

Which line are you expecting to fail? c? Why?

Sorry @RyanCavanaugh I didn't explain my test case correctly.

Even with the desired behaviour I think c would compile. I've edited my case to show what I expect to happen more clearly. (Namely the return type of Object.fromEntries taking the key type information from the tuple array instead of PropertyKey)

@RyanCavanaugh Would you mind if I attempted this and raised a PR please?

I believe this is also causing an error in our project. I have distilled the error down to the following simple repo which I think is the same issue @AWare is describing here.

package.json:

{
  "name": "tserror",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build-good": "rimraf index.js && tsc --build tsconfig.good.json",
    "build-bad": "rimraf index.js && tsc --build tsconfig.bad.json"
  },
  "devDependencies": {
    "typescript": "^3.5.2",
    "rimraf": "^2.6.3"
  }
}

tsconfig.good.json:

{
    "compilerOptions": {
        "lib": ["esnext"],
        "strict": true,
        "keyofStringsOnly": false,
    },
    "include": ["index.ts"],
    "exclude": ["node_modules"]
}

tsconfig.bad.json:

{
    "compilerOptions": {
        "lib": ["esnext"],
        "strict": true,
        "keyofStringsOnly": true,
    },
    "include": ["index.ts"],
    "exclude": ["node_modules"]
}

index.ts:

const moo: string = "oh oh" 

When I run npm run build-good this simple example builds as expected. However, if I then go and run npm run build-bad I get the following error:

Screen Shot 2019-06-17 at 8 16 08 PM

The only difference in the "bad" build is that i've set "keyofStringsOnly": true instead of false. This is a stripped down version of my actual configuration but I believe it is just the interaction of "keyofStringsOnly":true and lib:["esnext"] that is relevant to this issue.

Having same issue on my side, same configuration ("keyofStringsOnly": true)

Got the same issue randomly, not sure what triggered it as things were compiling fine. But using a target of either esnext or es2019 will cause this compilation issue. Also confirmed this by updating to typescript@next and running the same code.

Replication details are as follows, I did this in my /tmp directory and you can pretty much copy and paste these steps to reproduce.

tsconfig.json

{
  "compilerOptions": {
    "target": "es2019",
    "keyofStringsOnly": true,
  },
}

Running tsc will complain that it has nothing to do, so touch index.ts.
Then running the tsc command will result in the following error:

*chop*/typescript/lib/lib.es2019.object.d.ts:28:81 - error TS2322: Type 'string | number | symbol' is not assignable to type 'string'.
  Type 'number' is not assignable to type 'string'.

28     fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k in PropertyKey]: T };
                                                                                   ~~~~~~~~~~~

Found 1 error.

This is specifically part of es2019 (currently esnext aliases this I believe).
The fix, for now, is to "target": "es2018" instead in your tsconfig.json.

{
  "compilerOptions": {
    "target": "es2018",
    "keyofStringsOnly": true,
  },
}

I changed target to "target": "es2018", but I am still getting the following error:

node_modules/typescript/lib/lib.es2019.object.d.ts:28:81 - error TS2322: Type 'string | number | symbol' is not assignable to type 'string'.
  Type 'number' is not assignable to type 'string'.

28     fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k in PropertyKey]: T };

Here is my tsconfig.json

{
  "compilerOptions": {
    "target": "es2018",
    "moduleResolution": "node",
    "module": "commonjs",
    "sourceMap": true,
    "rootDir": "src",
    "outDir": "dist",
    "keyofStringsOnly": true,
    "lib": ["esnext", "dom"],
    "esModuleInterop": true,
  },
  "exclude": ["node_modules", "**/*.spec.ts"]
}

The only thing that works is removing "keyofStringsOnly": true,.

I am not sure why this is still happening. Has anyone else experienced this/ any suggestions on a fix?

It sounds like there are really two bugs here: the more serious is that the current definition of fromEntries errors when keyofStringsOnly: true. The cause (and likely fix) is similar to the original bug so I'm not going to open a new bug, though.

I see 3 possible fixes for these two bugs:

  1. Do nothing -- this has been a bug for a couple versions of TS already.
  2. Add a type parameter for the key type K extends PropertyKey. This is what @Aware suggests, but it doesn't fix the second bug.
  3. Simplify the key type to string instead of PropertyKey. This fixes both bugs raised here, but may break other code. The related issues the OP links have some discussion of this.

Because this isn't a 3.8 regression, I'm not going to fix this in the 3.8 beta period — any change to the DOM types is going to break someone. I'll look at this when 3.9 starts.

Is there a particularly good workaround for this in the meantime without setting keyofStringsOnly: false?

*edit - for now I have set "skipDefaultLibCheck": true which seems okay

My question is this: I want Object.fromEntries() to be generic on the key type, something like this:

fromEntries = <Key extends PropertyKey, Value>(entries: Iterable<[Key, Value]>) => { [key: Key]: Value }

Is there a motivation for why a more precise typing like that would be unsafe at the library level? I use enums a lot as keys and I'd like for mapping across them to preserve the fact that the keys are from my enum, not just a string or maybe a number or something.

I opened #37457 which makes the return type { [k: string]: T }. Compared to the current declaration, this just drops the numeric index signature.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Roam-Cooper picture Roam-Cooper  路  3Comments

weswigham picture weswigham  路  3Comments

manekinekko picture manekinekko  路  3Comments

jbondc picture jbondc  路  3Comments

uber5001 picture uber5001  路  3Comments