Static types for dynamically named properties by ahejlsberg · Pull Request #11929 · microsoft/TypeScript (original) (raw)
This PR adds new typing constructs that enable static validation of code involving dynamic property names and properties selected by such dynamic names. For example, in JavaScript it is fairly common to have APIs that expect property names as parameters, but so far it hasn't been possible to express the type relationships that occur in those APIs. The PR also improves error messages related to missing properties and/or index signatures.
The PR is inspired by #1295 and #10425 and the discussions in those issues. The PR implements two new type constructs:
- Index type queries of the form
keyof T
, whereT
is some type. - Indexed access types of the form
T[K]
, whereT
is some type andK
is a type that is assignable tokeyof T
(or assignable tonumber
ifT
contains a numeric index signature).
An index type query keyof T
yields the type of permitted property names for T
. A keyof T
type is considered a subtype of string
. When T
is not a type parameter, keyof T
is resolved as follows:
- If
T
has no apparent string index signature,keyof T
is a type of the form"p1" | "p2" | ... | "pX"
, where the string literals represent the names of the public properties ofT
. IfT
has no public properties,keyof T
is the typenever
. - If
T
has an apparent string index signature,keyof T
is the typestring
.
Note that keyof T
ignores numeric index signatures. To properly account for those we would need a numericstring
type to represent strings contain numeric representations.
An indexed access type T[K]
requires K
to be a type that is assignable to keyof T
(or assignable to number if T contains a numeric index signature) and yields the type of the property or properties in T
selected by K
. T[K]
permits K
to be a type parameter, in which case K
must be constrained to a type that is assignable to keyof T
. Otherwise, when K
is not a type parameter, T[K]
is resolved as follows:
- If
K
is a union typeK1 | K2 | ... | Kn
,T[K]
is equivalent toT[K1] | T[K2] | ... | T[Kn]
. - If
K
is a string literal type, numeric literal type, or enum literal type, andT
contains a public property with the name given by that literal type,T[K]
is the type of that property. - If
K
is a type assignable tonumber
andT
contains a numeric index signature,T[K]
is the type of that numeric index signature. - If
K
is a type assignable tostring
andT
contains a string index signature,T[K]
is the type of that string index signature.
Some examples:
interface Thing { name: string; width: number; height: number; inStock: boolean; }
type K1 = keyof Thing; // "name" | "width" | "height" | "inStock" type K2 = keyof Thing[]; // "length" | "push" | "pop" | "concat" | ... type K3 = keyof { [x: string]: Thing }; // string
type P1 = Thing["name"]; // string type P2 = Thing["width" | "height"]; // number type P3 = Thing["name" | "inStock"]; // string | boolean type P4 = string["charAt"]; // (pos: number) => string type P5 = string[]["push"]; // (...items: string[]) => number type P6 = string[][0]; // string
An indexed access type T[K]
permits K
to be a type parameter that is constrained to keyof T
. This makes it possible to do parametric abstraction over property access:
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // Inferred type is T[K] }
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) { obj[key] = value; }
function f1(thing: Thing, propName: "name" | "width") { let name = getProperty(thing, "name"); // Ok, type string let size = getProperty(thing, "size"); // Error, no property named "size" setProperty(thing, "width", 42); // Ok setProperty(thing, "color", "blue"); // Error, no property named "color" let nameOrWidth = getProperty(thing, propName); // Ok, type string | number }
function f2(tuple: [string, number, Thing]) { let length = getProperty(tuple, "length"); // Ok, type number const TWO = "2"; let t0 = getProperty(tuple, "0"); // Ok, type string let t1 = getProperty(tuple, "1"); // Ok, type number let t2 = getProperty(tuple, TWO); // Ok, type Thing }
class Component { props: PropType; getProperty(key: K) { return this.props[key]; } setProperty(key: K, value: PropType[K]) { this.props[key] = value; } }
function f3(component: Component) { let width = component.getProperty("width"); // Ok, type number component.setProperty("name", "test"); // Ok }
function pluck<T, K extends keyof T>(array: T[], key: K) { return array.map(x => x[key]); }
function f4(things: Thing[]) { let names = pluck(things, "name"); // string[] let widths = pluck(things, "width"); // number[] }
Note: This description has been edited to reflect the changes implemented in #12425.