Item 7: Think of Types as Sets of Values
May 10, 2024 ยท View on GitHub
Things to Remember
- Think of types as sets of values (the type's domain). These sets can either be finite (e.g.,
booleanor literal types) or infinite (e.g.,numberorstring). - TypeScript types form intersecting sets (a Venn diagram) rather than a strict hierarchy. Two types can overlap without either being a subtype of the other.
- Remember that an object can still belong to a type even if it has additional properties that were not mentioned in the type declaration.
- Type operations apply to a set's domain. The domain of
A | Bis the union of the domains ofAandB. - Think of "extends," "assignable to," and "subtype of" as synonyms for "subset of."
Code Samples
const x: never = 12;
// ~ Type 'number' is not assignable to type 'never'.
type A = 'A';
type B = 'B';
type Twelve = 12;
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
const a: AB = 'A'; // OK, value 'A' is a member of the set {'A', 'B'}
const c: AB = 'C';
// ~ Type '"C"' is not assignable to type 'AB'
// OK, {"A", "B"} is a subset of {"A", "B"}:
const ab: AB = Math.random() < 0.5 ? 'A' : 'B';
const ab12: AB12 = ab; // OK, {"A", "B"} is a subset of {"A", "B", 12}
declare let twelve: AB12;
const back: AB = twelve;
// ~~~~ Type 'AB12' is not assignable to type 'AB'
// Type '12' is not assignable to type 'AB'
type Int = 1 | 2 | 3 | 4 | 5 // | ...
interface Identified {
id: string;
}
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
type K = keyof (Person | Lifespan);
// ^? type K = never
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
interface NullyStudent {
name: string;
ageYears: number | null;
}
interface Student extends NullyStudent {
ageYears: number;
}
interface StringyStudent extends NullyStudent {
// ~~~~~~~~~~~~~~
// Interface 'StringyStudent' incorrectly extends interface 'NullyStudent'.
ageYears: number | string;
}
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }
interface Vector1D { x: number; }
interface Vector2D { x: number; y: number; }
interface Vector3D { x: number; y: number; z: number; }
function getKey<K extends string>(val: any, key: K) {
// ...
}
getKey({}, 'x'); // OK, 'x' extends string
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); // OK, 'a'|'b' extends string
getKey({}, document.title); // OK, string extends string
getKey({}, 12);
// ~~ Type 'number' is not assignable to parameter of type 'string'
const list = [1, 2];
// ^? const list: number[]
const tuple: [number, number] = list;
// ~~~~~ Type 'number[]' is not assignable to type '[number, number]'
// Target requires 2 element(s) but source may have fewer
const triple: [number, number, number] = [1, 2, 3];
const double: [number, number] = triple;
// ~~~~~~ '[number, number, number]' is not assignable to '[number, number]'
// Source has 3 element(s) but target allows only 2.
type T = Exclude<string|Date, string|number>;
// ^? type T = Date
type NonZeroNums = Exclude<number, 0>;
// ^? type NonZeroNums = number
interface Lockbox {
code: number;
}
interface ReadonlyLockbox {
readonly code: number;
}
const box: Lockbox = { code: 4216 };
const robox: ReadonlyLockbox = { code: 3625 };
box.code = 1234; // ok
robox.code = 1234;
// ~~~~ Cannot assign to 'code' because it is a read-only property.