There are no keys that TypeScript can guarantee belong to a value in the union type, so keyof for the union must be the empty set (never). Or, more formally:
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)
If you can build an intuition for why these equations hold, you’ll have come a long way toward understanding TypeScript’s type system!
interface Point {
x: number;
y: number;
}
type PointKeys = keyof Point; // Type is "x" | "y"
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
// ...
}
const pts: Point[] = [{x: 1, y: 1}, {x: 2, y: 0}];
sortBy(pts, 'x'); // OK, 'x' extends 'x'|'y' (aka keyof T)
sortBy(pts, 'y'); // OK, 'y' extends 'x'|'y'
sortBy(pts, Math.random() < 0.5 ? 'x' : 'y'); // OK, 'x'|'y' extends 'x'|'y'
sortBy(pts, 'z');
// ~~~ Type '"z"' is not assignable to parameter of type '"x" | "y"
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r: Room = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
// and 'elephant' does not exist in type 'Room'
}
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
};
const r2: Room = obj; // OK
- When you assign an object literal to a variable or pass it as an argument to a function, it undergoes excess property checking.
- Excess property checking is an effective way to find errors, but it is distinct from the usual structural assignability checks done by the TypeScript type checker. Conflating these processes will make it harder for you to build a mental model of assignability.
- Be aware of the limits of excess property checking: introducing an intermediate variable will remove these checks.
Ways to avoid duplication for the following two types:
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
interface Name {
first: string
last: string
}
type DancingDuo<T extends Name> = [T, T]
const couple1: DancingDuo<Name> = [
{ first: 'Fred', last: 'Astaire' },
{ first: 'Ginger', last: 'Rogers' },
] // OK
Thinking of types as sets of values, it helps to read “extends” as “subset of” here.
type Pick<T, K extends keyof T> = {
[k in K]: T[k]
}; // OK
This is also an example of index signatures. What should you use index signatures for? The canonical case is truly dynamic data.