microsoft/TypeScript

Typescript should be able to derive the type of the array created by `Object.keys` from the key signature of the object

quinn opened this issue · 7 comments

quinn commented

TypeScript Version: 3.2.0-dev.201xxxxx

Search Terms:
object.keys
Code

type Keys = 'one' | 'two'

const obj:{[key in Keys]: string} = {
  one: 'value for one',
  two: 'value for two',
}

Object.keys(obj).forEach(key =>
  console.log(obj[key])
)

Expected behavior:

No error.

Actual behavior:

$ tsc --noImplicitAny test.ts
test.ts:9:15 - error TS7017: Element implicitly has an 'any' type because type '{ one: string; two: string; }' has no index signature.

9   console.log(obj[key])
                ~~~~~~~~

Playground Link:

Related Issues:

Duplicate of #26901 (any many others).

quinn commented

@ahejlsberg thanks for the additional context. If this isn't possible, could someone at least explain or link to what is the most idiomatic typescript in these situations?

@quinn If you are certain obj has no other enumerable properties you can assert that using a type assertion. Either of the following:

type Keys = 'one' | 'two';

const obj:{[key in Keys]: string} = {
  one: 'value for one',
  two: 'value for two',
};

Object.keys(obj).forEach(key => console.log(obj[key as Keys]));

(Object.keys(obj) as Keys[]).forEach(key => console.log(obj[key]));
quinn commented

@ahejlsberg using as fixed it, thank you! My assumption would be that if I somehow mutated the object to have a key other than 'one' or 'two' the fact that i have a defined type for the key [key in Keys] typescript would catch that.

quinn commented

and I definitely feel like i'm not doing something right if i ever need to use as. I really hate to rely on being certain myself rather than letting typescript to its thing.

Emmm, same problem with for-in:

let a: ClassA;

for (let k in a) {

    console.log(a[k]); // [ts] Element implicitly has an 'any' type because type 'ClassA' has no index signature. [7017]
}

And must be written in this style:

let a: ClassA;
let k: keyof ClassA;
for (k in a) {

    console.log(a[k]); // That's okay.
}

So, why doesn't TS give k the type keyof ClassA automatically?

So let's say you write

const obj = { x: 1, y: 2 };

keyof typeof obj has type "x" | "y"

It would be bad if TypeScript allowed a value "z" to appear in an expression typed as "x" | "y". But that's exactly what's at stake here:

type Point = { x: number, y: number };

function fn(x: Point) {
  // Proposal: make this *not* an error
  for (let k in x) printXorY(k);
}

function printXorY(key: keyof Point) {
  switch(key) {
    case "x":
      console.log("is x");
      break;
    case "y":
      console.log("is y");
      break;
    default:
      throw new Error("HOW IS THIS HAPPENING");
  }
}

const p3 = { x: 1, y: 2, z: 3 };
fn(p3); // oops