keyof T
refers to the valid keys for the type T
(which you probably already knew). Adding [x]
after an interface or union type selects 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;
*/
The concept of "lookup types" seems to only be documented in the TypeScript 2.1 release notes here:
keyof
and Lookup Types
In JavaScript, it is common to have APIs that expect property names as parameters, but expressing the type relationships in those APIs has not been possible.
Index Type Query or keyof
introduces indexed type query which yields the permitted property names type 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
This leads us to indexed access types, also known as lookup types. They are written exactly like element accesses but are intended 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
This pattern can be used within other parts of the type system to perform 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 core aspect of FunctionPropertyNames<T>
generates an interface with members typed as never
for properties of T
that are not function-typed, while keeping the original member type for those that are. For example, as illustrated in the code snippet, by defining:
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T1 = FunctionPropertyNames<Part>;
The outcome for T1
will be "updatePart"
since that's the only function-typed property of Part
. Omitting the [keyof T]
section would result in the interface containing 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";
}
*/
The inclusion of [keyof T]
ensures that FunctionPropertyNames
delivers the names of the function-properties rather than the function-typed properties themselves (along with the never
-typed ones).