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:

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:

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:

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.