GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Support number and symbol named properties with keyof and mapped types by ahejlsberg · Pull Request #23592 · microsoft/TypeScript

Conversation

@ahejlsberg

@ahejlsberg Anders Hejlsberg (ahejlsberg) commented Apr 20, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

This PR adds support for number and symbol named properties in index types and mapped types. Previously, the keyof operator and mapped types only supported string named properties. Since this is technically a breaking change, the PR includes a --keyofStringsOnly compiler option that can be used to disable the new behavior in projects in which it causes errors (we expect this to be relatively rare).

Changes include:

  • An index type keyof T for some type T is a subtype of string | number | symbol. This is a breaking change as keyof T is no longer assignable to just string.
  • A mapped type { [P in K]: XXX } permits any K assignable to string | number | symbol.
  • In a for...in statement for an object of a generic type T, the inferred type of the iteration variable was previously keyof T but is now Extract<keyof T, string>. (In other words, the subset of keyof T that includes only string-like values.)

Given an object type X, keyof X is resolved as follows:

  • If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise
  • If X contains a numeric index signature, keyof X is a union of number and the literal types representing string-like and symbol-like properties, otherwise
  • keyof X is a union of the literal types representing string-like, number-like, and symbol-like properties.

The string-like properties of an object type are those declared using an identifier, a string literal, or a computed property name of a string literal type. The number-like properties of an object type are those declared using a numeric literal or computed property name of a numeric literal type. The symbol-like properties of an object type are those declared using a computed property name of a unique symbol type (see #15473).

In a mapped type { [P in K]: XXX }, each string literal type in K introduces a property with a string name, each numeric literal type in K introduces a property with a numeric name, and each unique symbol type in K introduces a property with a unique symbol name. Furthermore, if K includes type string, a string index signature is introduced, and if K includes type number, a numeric index signature is introduced.

Some examples:

const c = "c";
const d = 10;
const e = Symbol();

const enum E1 { A, B, C }
const enum E2 { A = "A", B = "B", C = "C" }

type Foo = {
    a: string;       // String-like name
    5: string;       // Number-like name
    [c]: string;     // String-like name
    [d]: string;     // Number-like name
    [e]: string;     // Symbol-like name
    [E1.A]: string;  // Number-like name
    [E2.A]: string;  // String-like name
}

type K1 = keyof Foo;  // "a" | 5 | "c" | 10 | typeof e | E1.A | E2.A
type K2 = Extract<keyof Foo, string>;  // "a" | "c" | E2.A
type K3 = Extract<keyof Foo, number>;  // 5 | 10 | E1.A
type K4 = Extract<keyof Foo, symbol>;  // typeof e

Since keyof now reflects the presence of a numeric index signature by including type number in the key type, mapped types such as Partial<T> and Readonly<T> work correctly when applied to object types with numeric index signatures:

type Arrayish<T> = {
    length: number;
    [x: number]: T;
}

type ReadonlyArrayish<T> = Readonly<Arrayish<T>>;

declare const map: ReadonlyArrayish<string>;
let n = map.length;
let x = map[123];  // Previously of type any (or an error with --noImplicitAny)

Furthermore, with the keyof operator's support for number and symbol named keys, it is now possible to abstract over access to properties of objects that are indexed by numeric literals (such as numeric enum types) and unique symbols.

const enum Enum { A, B, C }

const enumToStringMap = {
    [Enum.A]: "Name A",
    [Enum.B]: "Name B",
    [Enum.C]: "Name C"
}

const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();

const symbolToNumberMap = {
    [sym1]: 1,
    [sym2]: 2,
    [sym3]: 3
};

type KE = keyof typeof enumToStringMap;     // Enum (i.e. Enum.A | Enum.B | Enum.C)
type KS = keyof typeof symbolToNumberMap;   // typeof sym1 | typeof sym2 | typeof sym3

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

let x1 = getValue(enumToStringMap, Enum.C);  // Returns "Name C"
let x2 = getValue(symbolToNumberMap, sym3);  // Returns 3

Fixes #13715.
Fixes #14359.
Fixes #20721.
Fixes #21983.
Fixes #22105.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

dinoboff, alexrock, yortus, j-oliveras, ikatyang, kobezzza, yanchith, pelotom, aboyton, SlurpTheo, and 19 more reacted with thumbs up emoji kpdonn, artaommahe, j-oliveras, s-ve, mpawelski, benbraou, wizarrc, aaronbeall, begincalendar, amitbeck, and 9 more reacted with hooray emoji amitbeck, Bnaya, chelmertz, janriemer, alitaheri, and Vaelor reacted with heart emoji
All reactions
  • 👍 29 reactions
  • 🎉 19 reactions
  • ❤️ 6 reactions
Anders Hejlsberg (ahejlsberg) added 25 commits April 5, 2018 06:25
@ahejlsberg
Move nameType from SymbolLinks to TransientSymbol
a8d6a45
@ahejlsberg
Move nameType to base Symbol, generate nameType properties where missing
39bb93f
@ahejlsberg
Limit getLiteralTypeFromPropertyName to only return string-like types
fc85ba9
@ahejlsberg
Accept new baselines
ccf20d3
@ahejlsberg
Add support for numbers and symbols in keyof (but keep it disabled)
ff20f38
@ahejlsberg
Move 'PropertyKey' from es2015.core.d.ts to es5.d.ts
68ce69a
@ahejlsberg
Accept new baselines
2c02195
@ahejlsberg
Enable 'keyof T' for full string | number | symbol
6d93f30
@ahejlsberg
Update tests
0379666
@ahejlsberg
Accept new baselines
b1545fe
@ahejlsberg
String index signatures do not apply to symbols
b40592c
@ahejlsberg
Update test
0a37cd3
@ahejlsberg
Accept new baselines
b746f8e
@ahejlsberg
Fixes and improvements to indexed access type relationships
c344e6d
@ahejlsberg
Accept new baselines
6c60f7e
@ahejlsberg
Add members for numeric and symbol named properties in mapped types
8cb515a
@ahejlsberg
Merge branch 'master' into improveIndexTypes
16cd558
# Conflicts:
#	src/compiler/checker.ts
#	src/server/utilities.ts
@ahejlsberg
Don't widen unique symbol types during type inference
b11be80
@ahejlsberg
Accept new baselines
02534cc
@ahejlsberg
For 'T extends { [x: string]: XXX }' constraint of T[keyof T] is XXX
b14d389
@ahejlsberg
Accept new baselines
254782c
@ahejlsberg
Revise IndexType to have stringsOnly property
9e4e215
@ahejlsberg
Update test
5f0d880
@ahejlsberg
Properties with numeric names have numeric literal types in keyof T
eb7bbfb
@ahejlsberg
Accept new baselines
b38e42e
@ahejlsberg
Anders Hejlsberg (ahejlsberg) requested review from Daniel Rosenwasser (DanielRosenwasser), Mohamed Hegazy (mhegazy), Ron Buckton (rbuckton) and Wesley Wigham (weswigham) April 20, 2018 23:08
@ahejlsberg Anders Hejlsberg (ahejlsberg) mentioned this pull request Apr 20, 2018
Consider adding symbolof type operator, like keyof but for unique symbol properties #20721
Closed
@kpdonn Kevin Donnelly (kpdonn) mentioned this pull request Apr 24, 2018
Bug from #23592 - Type 'keyof U & K' does not satisfy the constraint 'string | number | symbol' #23648
Closed
@jcalz Joe Calzaretta (jcalz) mentioned this pull request Apr 24, 2018
Cannot map over intersection of string with string literal (e.g., "a" & string) #23651
Closed
@rbuckton

Ron Buckton (rbuckton) commented Apr 25, 2018

Copy link
Copy Markdown
Contributor

Luke Garrison (@lkgarrison), that package has not yet been updated to use unique symbol, though a PR will be posted shortly.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rbuckton Ron Buckton (rbuckton) mentioned this pull request Apr 25, 2018
Compatibility fix for keyof T in ts 2.9 DefinitelyTyped/DefinitelyTyped#25271
Merged
7 tasks done
@andnp Andy Patterson (andnp) mentioned this pull request Apr 25, 2018
feat: fix typescript 2.9 errors andnp/SimplyTyped#36
Merged
This was referenced Apr 26, 2018
Mapped generic types lose index signature #23080
Closed
Enums can not be used for index signature types #13042
Closed
error TS2677: Type 'keyof E' is not assignable to type 'string' #23724
Closed
This was referenced Apr 30, 2018
core-js: Comment out dict[sym] test DefinitelyTyped/DefinitelyTyped#25420
Merged
es6-shim: Avoid conflict with global PropertyKey type DefinitelyTyped/DefinitelyTyped#25421
Merged
ramda: Use [email protected] DefinitelyTyped/DefinitelyTyped#25424
Merged
Ethan Resnick (ethanresnick) added a commit to ethanresnick/json-api that referenced this pull request May 3, 2018
@ethanresnick
fix(typings): tweak typings to work with ts@next
660f93f
See microsoft/TypeScript#23592
This was referenced May 3, 2018
Non-null assertion doesn't work for mapped types #23849
Closed
Look at simplified types when checking distributive conditional constraints #23884
Merged
@KiaraGrouwstra kiara (KiaraGrouwstra) mentioned this pull request May 6, 2018
use 2.9 number/symbol keyof KiaraGrouwstra/typical#23
Open
@mhegazy Mohamed Hegazy (mhegazy) mentioned this pull request May 8, 2018
Update schema for TS 2.9 flags SchemaStore/schemastore#451
Merged
@chriseppstein Chris Eppstein (chriseppstein) mentioned this pull request May 9, 2018
Confusing aspects of recent improvements to Index Types #23994
Closed
@massimonewsuk massimonewsuk mentioned this pull request May 18, 2018
Can't remove optional modifier from properties on tree #24250
Closed
@UselessPickles

Jeff Lau (UselessPickles) commented May 22, 2018

Copy link
Copy Markdown

Any recommendations for how library developers should proceed with this breaking change?

For example, I maintain a project available via npm, written in typescript, and distribute its type definitions file (which includes generic types and method signatures involving keyof T) with the npm package. It currently depends on keyof only returning string types, and, up til now, was compatible with all TypeScript versions since 2.3.

I could modify the source code to compile in TypeScript 2.9 by replacing all instances of keyof T with Extract<keyof T, string>, but then the type definitions file that I generate/distribute will cause compiler errors for anyone attempting to use my npm package with TypeScript versions 2.3-2.7 (because Extract is new for 2.8).

Are there any tricks I can use to define my generic classes/methods involving keyof T so that they are valid for for both version 2.9 and pre-2.9, without requiring all users of my package to use the --keyofStringsOnly compiler option?

Is there some way to conditionally define a generic type differently depending on TypeScript versions. I basically need (example inspired by C compiler directives):

#if TYPESCRIPT_VERSION >= 2.9
type ExtractStringKeysType<T> = Extract<keyof T, string>;
#else
type ExtractStringKeysType<T> = keyof T;
#endif
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@weswigham

Wesley Wigham (weswigham) commented May 22, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

keyof T & string is a generally backcompat way to mostly accomplish the same thing as the extract type.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented May 22, 2018

Copy link
Copy Markdown
Member Author

Jeff Lau (@UselessPickles) I recommend using keyof any as the constraint for type parameters that represent property names (i.e. change from T extends string to T extends keyof any). keyof any resolves to string in previous versions and string | number | symbol in 2.9 so it is effectively a conditional compilation construct.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@UselessPickles

Jeff Lau (UselessPickles) commented May 22, 2018

Copy link
Copy Markdown

Wesley Wigham (@weswigham) Thanks, I'll give that a try and report back to confirm that it works (at least for TS versions 2.3-2.9). If this is the solution, I think it should be added as a suggestion to the breaking changes documentation.

Anders Hejlsberg (@ahejlsberg) That is a very useful tip (maybe should be added to the breaking changes documentation?), but actually seems to be opposite of what I was looking for (a way to consistently get only the string-like key types of some type T in all TS versions).

Some more context...

The project I'm trying to future-proof for TS 2.9: https://github.com/UselessPickles/ts-enum-util

At the core of the problem is that I use this type as a constraint to support any "enum-like" types (objects with string keys and number | string values), but in a way that allows the exact keys and exact value types to be inferred properly for string enums, etc):

export type EnumLike<V extends number | string, K extends string> = {
    [P in K]: V
};

Then the main generic class that uses that type constraint:

export class EnumWrapper<
    V extends number | string = number | string,
    T extends EnumLike<V, keyof T> = any
>

Which allows me to have a very strictly typed utility wrapper for any enum that knows the exact type of values in the enum (V), and the exact union of string literal enum names/keys (keyof T).

I think changing my constraint in EnumWrapper to T extends EnumLike<V, keyof T & string> is more appropriate than changing the constraint in EnumLike to K extends keyof any, because enums should only have string keys.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@UselessPickles

Jeff Lau (UselessPickles) commented May 23, 2018

Copy link
Copy Markdown

Wesley Wigham (@weswigham) Unfortunately, the keyof T & string trick doesn't work for me :(. It only works in TS 2.9.

Prior to TS 2.9, the & string part sticks around as part of the type, and prevents me from using it to index into type T to get the type of the properties in T. For example:

interface Test {
    A: number;
    B: string;
}

type StringKeys<T> = keyof T & string;
type StringKeyValues<T> = T[StringKeys<T>];

// expect: "A" | "B"
// got: ("A" & string) | ("B" & string)
type Keys = StringKeys<Test>;

// expect: number | string
// got: any
type Values = StringKeyValues<Test>;
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@seaBubble

Marta Galve (seaBubble) commented May 24, 2018

Copy link
Copy Markdown

I am trying to set an enum as the key of an interface in 2.7.2:
enum TestEnum {
A: 'a',
B: 'a'
}
interface TestCounter {
[key: TestEnum]: number;
}

so I can have objects as {'a':1}

Am I doing something wrong?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@UselessPickles

Jeff Lau (UselessPickles) commented May 24, 2018

Copy link
Copy Markdown

Marta Galve (@seaBubble) The type of the index signature key can only be number or string. You have to use a mapped type to create an interface whose keys are the values of a string enum:

enum TestEnum {
  A = 'a',
  B = 'b'
}

type TestCounter = { [P in TestEnum]: number };

const testCounter: TestCounter = {
    [TestEnum.A]: 0,
    [TestEnum.B]: 0
};

This can be further generalized with generics, and can also work with string literal unions. Check out my generic strictly typed string visitor/mapper utility for more advanced generic examples: https://github.com/UselessPickles/ts-string-visitor

In particular, look at the StringMapperCore interface in src/StringMapper.ts.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zpdDG4gta8XKpMCd

ZpdDG4gta (zpdDG4gta8XKpMCd) commented May 27, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Anders Hejlsberg (@ahejlsberg) will there be support for

enum E = { A, B }
declare const keyedByEnum: { [ P in E ]: any; }
for (const key in keyedByEnum) {
   // key needs to be of type E
}

and

const keys = Object.keys(keyedByEnum); // keys need to be E[]

?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mhegazy

Mohamed Hegazy (mhegazy) commented May 27, 2018

Copy link
Copy Markdown
Contributor

In both cases the key is a string.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zpdDG4gta8XKpMCd

ZpdDG4gta (zpdDG4gta8XKpMCd) commented May 27, 2018 via email

Copy link
Copy Markdown
bummer, you are right
…
On Sat, May 26, 2018, 21:49 Mohamed Hegazy ***@***.***> wrote: In both cases the key is a string. — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#23592 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AA5PzcbfZaUivSVSVGKnjp_CXnKCbHS6ks5t2gYdgaJpZM4TeMpQ> .
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ikatyang Ika (ikatyang) mentioned this pull request Jul 25, 2018
feat: compatible with keyofStringsOnly typed-typings/npm-ramda#404
Merged
@microsoft Microsoft (microsoft) locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@weswigham Wesley Wigham (weswigham) weswigham left review comments
@rbuckton Ron Buckton (rbuckton) Awaiting requested review from rbuckton
@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) Awaiting requested review from DanielRosenwasser
+1 more reviewer
@mhegazy Mohamed Hegazy (mhegazy) mhegazy approved these changes
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

Breaking Change Would introduce errors in existing code

Projects

None yet

Milestone

TypeScript 2.9

Development

Successfully merging this pull request may close these issues.

keyof {[n: number]: any} should be string, not never keyof any incorrectly maps (only) to type string Consider adding symbolof type operator, like keyof but for unique symbol properties keyof and dynamic properties number should be subtype of keyof T[]

Uh oh!

There was an error while loading. Please reload this page.

11 participants

@ahejlsberg @sanex3339 @zpdDG4gta8XKpMCd @NN--- @mhegazy @lkgarrison @MartinJohns @rbuckton @UselessPickles @weswigham @seaBubble
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.