TypeScript: keyof any incorrectly maps (only) to type string

TypeScript Version: 2.7.1 and 2.8.0-dev.20180215

Search Terms: “keyof any”

Code

function foo<T>(bar: T, baz: keyof T) {
        console.log(bar[baz]);
}

const sym = Symbol();
const quirk = { [sym]: "thing" };

foo<any>(quirk, sym);

Expected behavior: keyof any should be equivalent to PropertyKey (that is string | number | symbol)

The example code should compile without error.

Actual behavior: keyof any is equivalent to string

The example code does not compile and produces error: error TS2345: Argument of type 'unique symbol' is not assignable to parameter of type 'string'.

Playground Link: https://www.typescriptlang.org/play/#src=function foo<T>(bar%3A T%2C baz%3A keyof T) { console.log(bar[baz])%3B } const sym %3D Symbol()%3B const quirk %3D { [sym]%3A "thing" }%3B foo<any>(quirk%2C sym)%3B

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 25 (3 by maintainers)

Most upvoted comments

I was about to agree but thought it through a bit more. 😛

To answer your question @yortus I can’t use PropertyKey because in the vast majority of cases the type T is known and so to use anything but keyof T would introduce the possibility of typos.

There is no reason for keyof to maintain consistency with Object.keys() because it’s only used to ensure type safety. There is an argument that the ES spec is flawed and 7.3.21 point 4.a. should be changed to include symbols but sure that might break existing code.

In effect the type checking provided by keyof is plain and simply wrong if it doesn’t include symbol keys. The only times it will be used is to ensure nobody tries to improperly index an object. Right now it’s preventing people from properly indexing an object.

Also the ECMAScript spec (current draft) is inconsistent itself on this:

6.1.7.3 Invariants of the Essential Internal Methods

In this section we have a declaration that keys are either strings or symbols.

[[OwnPropertyKeys]] ( ) The return value must be a List. The returned list must not contain any duplicate entries. The Type of each element of the returned List is either String or Symbol. The returned List must contain at least the keys of all non-configurable own properties that have previously been observed. If the object is non-extensible, the returned List must contain only the keys of all own properties of the object that are observable using [[GetOwnProperty]].

Other sections that assert this include (I didn’t do an exhaustive check): https://tc39.github.io/ecma262/#sec-topropertykey https://tc39.github.io/ecma262/#sec-ispropertykey

So in effect we have a standard that accepts property keys are strings and symbols, a backwards compatibility (presumably) to ensure legacy JS code doesn’t break at runtime and a type checker that is observing the legacy runtime characteristics rather than the actual type characteristics of properties. The type checker itself also conceding (by the definition of PropertyKey) that a generic property of any old object should conform to compilation-time type checking expectations and not runtime expectations of legacy code.

Legacy JS doesn’t benefit from Typescript enforcing what is already enforced by the runtime, yet Typescript code suffers because of the inconsistency outlined in the examples already given.

With the element type of the array returned by Object.keys always being a subset of the types of PropertyKey there’s no danger there. But by keyof T being a subset of the possible actual property key types we end up with broken type checking that excludes possible values.

Finally symbolof has no practical purpose once keyof is fixed. If it were to be introduced then it would be equally valid to introduce stringof to complement it if it’s accepted that keyof is currently broken.

Phew

Edit: I apparently wrote keysof in some places instead of keyof.

What’s your reasoning behind:

  1. Most of the time people are going to need keyof more than symbolof; and
  2. Having keyof only refer to string keys can be useful

If that is the case then it seems to me to be more explicit/clear to have keyof, stringof and symbolof?

Thanks 😃

Whilst there are technically no number keys in reality they are often used and it would be considered inappropriate to call toString every time you wanted to index an array. In addition we already have PropertyKey defined as the three types.

It makes sense to introduce some consistency and use PropertyKey everywhere a key could reasonably be used.

That would include in interface definitions and in keyof any.

Right now keyof any is string, interface index keys are string|number, PropertyKey is string|number|symbol and in actual usage of an object we use PropertyKey.

This isn’t a duplicate because I’m proposing that the inconsistency is the bug and symbolsof would do nothing useful but increase the chaos that we’re already faced with!

@ogwh that could be fixed either way: either by (a) adding symbols to keyof, or by (b) adding symbolof and redefining Readonly<T> to use keyof T | symbolof T.

So yes, that is a problem with the current definitions, but its not necessary an argument for one approach over the other.

Having keyof only refer to string keys can be useful, introducing symbolof seems to be the right way to go. My guess is most of the time people are going to need keyof more than symbolof anyway. If someone consistently needs both they can just create a type alias type SymbolKeyOf<T> = keyof T | symbolof T.

As for making keyof return symbols… Object.keys only returns string keys at runtime, and many other JS constructs only operate over string keys. I think we may do symbolof operator to maintain compatibility and keep a distinction between the two namespaces.

@ogwh that was in @weswigham’s comment for why keyof should be just strings.

As for keyof any, since this is not referring to the keys of any particular object, why not just use the PropertyKey type in your annotation instead?

(There are no number property keys. All non-symbol property keys are either strings or numeric strings.)