keyof T
means valid keys for the T
type (you probably knew that). When you have [x]
after an interface or union type, it picks the type of the property/member with the name x
. For example (playground link):
interface Example {
a: number;
b: string;
}
type E1 = Example["a"];
/*
type E1 = number;
*/
As far as I know, these "lookup types" are only documented in the TypeScript 2.1 release notes here:
keyof
and Lookup Types
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.
Enter Index Type Query or keyof
; An indexed type query keyof T
yields the type of permitted property names for T. A keyof T
type is considered a subtype of string.
Example
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
The dual of this is indexed access types, also called lookup types. Syntactically, they look exactly like an element access, but are written as types:
Example
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string
You can use this pattern with other parts of the type system to get type-safe lookups.
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;
}
let x = { foo: 10, bar: "hello!" };
let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string
let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"
setProperty(x, "foo", "string"); // Error!, string expected number
The main part of FunctionPropertyNames<T>
produces an interface with never
-typed members for all the properties of T
that aren't function-typed, and the original member type for those that are. For instance, as shown in the example, if you do this:
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T1 = FunctionPropertyNames<Part>;
T1
ends up being "updatePart"
because that's the only key of T
(Part
) that has a function type. Without the [keyof T]
part, you'd get the interface with the never
members instead (playground link):
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type Example<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}/* Same as above, but without [keyof T] here*/;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T1 = FunctionPropertyNames<Part>;
/*
type T1 = "updatePart"
*/
type E1 = Example<Part>;
/*
type E1 = {
id: never;
name: never;
subparts: never;
updatePart: "updatePart";
}
*/
It's the [keyof T]
part that makes FunctionPropertyNames
provide the names of the function-typed properties, rather than the function-typed properties themselves (and never
-typed ones).