TypeScript Basic Notes
Modules
Classic Module Resolution
import { a } from './module'
:/root/src/folder/module.ts
./root/src/folder/module.d.ts
.
import { a } from 'module'
:/root/src/folder/module.ts
./root/src/folder/module.d.ts
./root/src/module.ts
./root/src/module.d.ts
./root/module.ts
./root/module.d.ts
./module.ts
./module.d.ts
.
Node Module Resolution
const x = require('./module')
:/root/src/module.ts
./root/src/module.tsx
./root/src/module.d.ts
./root/src/module/package.json
+{ "types": "lib/mainModule.ts" }
=/root/src/module/lib/mainModule.ts
./root/src/module/index.ts
./root/src/module/index.tsx
./root/src/module/index.d.ts
.
const x = require('module')
:/root/src/node_modules/module.ts
./root/src/node_modules/module.tsx
./root/src/node_modules/module.d.ts
./root/src/node_modules/module/package.json
(if it specifies atypes
property)./root/src/node_modules/@types/module.d.ts
./root/src/node_modules/module/index.ts
./root/src/node_modules/module/index.tsx
./root/src/node_modules/module/index.d.ts
./root/node_modules/module.ts
./root/node_modules/module.tsx
./root/node_modules/module.d.ts
./root/node_modules/module/package.json
(if it specifies atypes
property)./root/node_modules/@types/module.d.ts
./root/node_modules/module/index.ts
./root/node_modules/module/index.tsx
./root/node_modules/module/index.d.ts
./node_modules/module.ts
./node_modules/module.tsx
./node_modules/module.d.ts
./node_modules/module/package.json
(if it specifies atypes
property)./node_modules/@types/module.d.ts
./node_modules/module/index.ts
./node_modules/module/index.tsx
./node_modules/module/index.d.ts
.
Enum Types
const
enums don’t have representation at runtime,
its member values are used directly.1// Source code:2const enum NoYes {3 No,4 Yes,5}67function toGerman(value: NoYes) {8 switch (value) {9 case NoYes.No:10 return 'Neither';11 case NoYes.Yes:12 return 'Ja';13 }14}1516// Compiles to:17function toGerman(value) {18 switch (value) {19 case 'No' /* No */:20 return 'Neither';21 case 'Yes' /* Yes */:22 return 'Ja';23 }24}
Non-const enums are objects:
1// Source code:2enum Tristate {3 False,4 True,5 Unknown,6}78// Compiles to:9let Tristate;10(function (Tristate) {11 Tristate[(Tristate.False = 0)] = 'False';12 Tristate[(Tristate.True = 1)] = 'True';13 Tristate[(Tristate.Unknown = 2)] = 'Unknown';14})(Tristate || (Tristate = {}));1516console.log(Tristate[0]); // 'False'17console.log(Tristate.False); // 018console.log(Tristate[Tristate.False]); // 'False' because `Tristate.False == 0`
1enum NoYes {2 No = 'NO!',3 Yes = 'YES!',4}56let NoYes;7(function (NoYes) {8 NoYes.No = 'NO!';9 NoYes.Yes = 'YES!';10})(NoYes || (NoYes = {}));
Interface
- Type aliases may not participate in declaration merging, but interfaces can.
- Interfaces may only be used to declare the shapes of object, not re-name primitives.
- The key distinction is that a type cannot be re-opened to add new properties, an interface which is always extendable.
1interface Window {2 title: string;3}45interface Window {6 ts: TypeScriptAPI;7}89const src = 'const a = "Hello World"';10window.ts.transpileModule(src, {});
Index Signature
1const MyArray = [2 { name: 'Alice', age: 15 },3 { name: 'Bob', age: 23 },4 { name: 'Eve', age: 38 },5];67type Person = typeof MyArray[number];8// type Person = {9// name: string;10// age: number;11// }1213type Age = typeof MyArray[number]['age'];14// type Age = number1516type Age2 = Person['age'];17// type Age2 = number
{ [K in keyof T]: indexedType }[keyof T]
返回键名 (键名组成的联合类型):1type PickByValueType<T, ValueType> = Pick<2 T,3 { [K in keyof T]-?: T[K] extends ValueType ? K : never }[keyof T]4>;56type OmitByValueType<T, ValueType> = Pick<7 T,8 { [K in keyof T]-?: T[K] extends ValueType ? never : K }[keyof T]9>;1011type RequiredKeys<T> = {12 [K in keyof T]-?: {} extends Pick<T, K> ? never : K;13}[keyof T];1415type OptionalKeys<T> = {16 [K in keyof T]-?: {} extends Pick<T, K> ? K : never;17}[keyof T];1819type FunctionTypeKeys<T extends object> = {20 [K in keyof T]-?: T[K] extends Function ? K : never;21}[keyof T];2223type Filter<T extends object, ValueType> = {24 [K in keyof T as ValueType extends T[K] ? K : never]: T[K];25}; // Filter<{name: string; id: number;}, string> => {name: string;}
Template Literal Types
- Based on literal types.
- 4 intrinsic String Manipulation Types:
Uppercase<StringType>
.Lowercase<StringType>
.Capitalize<StringType>
.Uncapitalize<StringType>
.
1interface PropEventSource<Type> {2 on<Key extends string & keyof Type>(3 eventName: `${Key}Changed`,4 callback: (newValue: Type[Key]) => void5 ): void;6}78// Create a "watched object" with an 'on' method9// so that you can watch for changes to properties.10declare function makeWatchedObject<Type>(11 obj: Type12): Type & PropEventSource<Type>;1314const person = makeWatchedObject({15 firstName: 'Yi',16 lastName: 'Long',17 age: 26,18});1920person.on('firstNameChanged', newName => {21 // (parameter) newName: string22 console.log(`new name is ${newName.toUpperCase()}`);23});2425person.on('ageChanged', newAge => {26 // (parameter) newAge: number27 if (newAge < 0) {28 console.warn('warning! negative age');29 }30});3132// It's typo-resistent33person.on('firstName', () => {});34// Argument of type '"firstName"' is not assignable to35// parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.3637person.on('fstNameChanged', () => {});38// Argument of type '"fstNameChanged"' is not assignable to39// parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
Generic Types
1interface Lengthwise {2 length: number;3}45function createList<T extends number | Lengthwise>(): T[] {6 return [] as T[];7}89const numberList = createList<number>(); // ok10const stringList = createList<string>(); // ok11const arrayList = createList<any[]>(); // ok12const boolList = createList<boolean>(); // error
在类型编程里, 泛型就是变量:
1function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {2 return keys.map(key => obj[key]);3}
Union Types
1interface Square {2 kind: 'square';3 size: number;4}56interface Rectangle {7 kind: 'rectangle';8 width: number;9 height: number;10}1112interface Circle {13 kind: 'circle';14 radius: number;15}1617type Shape = Square | Rectangle | Circle;1819function area(s: Shape) {20 switch (s.kind) {21 case 'square':22 return s.size * s.size;23 case 'rectangle':24 return s.width * s.height;25 case 'circle':26 return Math.PI * s.radius ** 2;27 default: {28 const _exhaustiveCheck: never = s;29 return _exhaustiveCheck;30 }31 }32}
Intersection Types
intersection
type 具有所有类型的功能:1function extend<T, U>(first: T, second: U): T & U {2 const result = {} as T & U;3 for (const id in first) {4 (result as T)[id] = first[id];5 }6 for (const id in second) {7 if (!Object.prototype.hasOwnProperty.call(result, id)) {8 (result as U)[id] = second[id];9 }10 }1112 return result;13}1415const x = extend({ a: 'hello' }, { b: 42 });1617// 现在 x 拥有了 a 属性与 b 属性18const a = x.a;19const b = x.b;
Conditional Types
- Basic conditional types just like
if else
statement. - Nested conditional types just like
switch case
statement. - Distributive conditional types just like
map
statement (loop
statement) onunion
type. - Conditional types make TypeScript become real programing type system: TypeScript type system is Turing Complete.
- Conditional types in which checked type is
naked type parameter
are called DCT. - DCT are automatically distributed over union types during instantiation.
- When conditional types act on a generic type, they become distributive when given a union type.
( A | B | C ) extends T ? X : Y
相当于(A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
.- 没有被额外包装的联合类型参数, 在条件类型进行判定时会将联合类型分发, 分别进行判断.
1// "string" | "function"2type T1 = TypeName<string | (() => void)>;34// "string" | "object"5type T2 = TypeName<string | string[]>;67// "object"8type T3 = TypeName<string[] | number[]>;
1type Naked<T> = T extends boolean ? 'Y' : 'N';2type Wrapped<T> = [T] extends [boolean] ? 'Y' : 'N';34/*5 * 先分发到 Naked<number> | Naked<boolean>6 * 结果是 "N" | "Y"7 */8type Distributed = Naked<number | boolean>;910/*11 * 不会分发 直接是 [number | boolean] extends [boolean]12 * 结果是 "N"13 */14type NotDistributed = Wrapped<number | boolean>;
Mapped Types
Builtin Mapped Types
Basic Mapped Types
1type Readonly<T> = { readonly [P in keyof T]: T[P] };2type Partial<T> = { [P in keyof T]?: T[P] };3type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] };4type Required<T> = { [P in keyof T]-?: T[P] };5type Nullable<T> = { [P in keyof T]: T[P] | null };6type NonNullable<T> = T extends null | undefined ? never : T;7type Clone<T> = { [P in keyof T]: T[P] };8type Stringify<T> = { [P in keyof T]: string };
Union Mapped Types
With distributive conditional type:
1type Extract<T, U> = T extends U ? T : never;2type Exclude<T, U> = T extends U ? never : T;
Key Mapped Types
1type Pick<T, K extends keyof T> = { [P in K]: T[P] };2type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;3type Record<K extends keyof any, T> = { [P in K]: T };
Function Mapped Types
1type Parameters<T extends (...args: any) => any> = T extends (2 ...args: infer P3) => any4 ? P5 : never;67type ConstructorParameters<T extends new (...args: any) => any> =8 T extends new (...args: infer P) => any ? P : never;910type ReturnType<T extends (...args: any) => any> = T extends (11 ...args: any[]12) => infer R13 ? R14 : any;1516type InstanceType<T extends new (...args: any) => any> = T extends new (17 ...args: any18) => infer R19 ? R20 : any;2122type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any23 ? U24 : unknown;
Custom Mapped Types
Combine with:
in keyof
.readonly
.?
.-
.as
.- Template literal types.
- Conditional types.
- Builtin types.
- Other mapped types.
- Other custom types.
1// Removes 'readonly' attributes from a type's properties2type Mutable<Type> = {3 -readonly [Property in keyof Type]: Type[Property];4};56interface LockedAccount {7 readonly id: string;8 readonly name: string;9}1011type UnlockedAccount = Mutable<LockedAccount>;12// type UnlockedAccount = {13// id: string;14// name: string;15// };
1// Mapped types via `as` type2type Getters<Type> = {3 [Property in keyof Type as `get${Capitalize<4 string & Property5 >}`]: () => Type[Property];6};78interface Person {9 name: string;10 age: number;11 location: string;12}1314type LazyPerson = Getters<Person>;15// type LazyPerson = {16// getName: () => string;17// getAge: () => number;18// getLocation: () => string;19// }
1// Remove the 'kind' property2type RemoveKindField<Type> = {3 [Property in keyof Type as Exclude<Property, 'kind'>]: Type[Property];4};56interface Circle {7 kind: 'circle';8 radius: number;9}1011type KindlessCircle = RemoveKindField<Circle>;12// type KindlessCircle = {13// radius: number;14// }
1// Mapped type via conditional type2type ExtractPII<Type> = {3 [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;4};56interface DBFields {7 id: { format: 'incrementing' };8 name: { type: string; pii: true };9}1011type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;12// type ObjectsNeedingGDPRDeletion = {13// id: false;14// name: true;15// }
Utility Types
Null Types
1type Nullish = null | undefined;2type Nullable<T> = T | null;3type NonUndefinedable<A> = A extends undefined ? never : A;4type NonNullable<T> = T extends null | undefined ? never : T;
Boolean Types
1type Falsy = false | '' | 0 | null | undefined;2const isFalsy = (val: unknown): val is Falsy => !val;
Primitive Types
1type Primitive = string | number | boolean | bigint | symbol | null | undefined;23const isPrimitive = (val: unknown): val is Primitive => {4 if (val === null || val === undefined) {5 return true;6 }78 const typeDef = typeof val;910 const primitiveNonNullishTypes = [11 'string',12 'number',13 'bigint',14 'boolean',15 'symbol',16 ];1718 return primitiveNonNullishTypes.includes(typeDef);19};
Promise Types
1// TypeScript 4.5.2// Get naked Promise<T> type.3type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;45// A = string.6type A = Awaited<Promise<string>>;78// B = number.9type B = Awaited<Promise<Promise<number>>>;1011// C = boolean | number.12type C = Awaited<boolean | Promise<number>>;
Proxy Types
1interface Proxy<T> {2 get(): T;3 set(value: T): void;4}56type Proxify<T> = { [P in keyof T]: Proxy<T[P]> };
Recursive Types
1type DeepReadonly<T> = {2 +readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];3};45type DeepMutable<T> = {6 -readonly [P in keyof T]: T[P] extends object ? DeepMutable<T[P]> : T[P];7};89type DeepPartial<T> = {10 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];11};1213type DeepRequired<T> = {14 [P in keyof T]-?: T[P] extends object | undefined ? DeepRequired<T[P]> : T[P];15};
Lodash Types
1type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Type Inference
类型系统在获得足够的信息后,
能将 infer 后跟随的类型参数推导出来,
最后返回这个推导结果:
1type Parameters<T extends (...args: any) => any> = T extends (2 ...args: infer P3) => any4 ? P5 : never;67type ConstructorParameters<T extends new (...args: any) => any> =8 T extends new (...args: infer P) => any ? P : never;910type ReturnType<T extends (...args: any) => any> = T extends (11 ...args: any[]12) => infer R13 ? R14 : any;1516type InstanceType<T extends new (...args: any) => any> = T extends new (17 ...args: any18) => infer R19 ? R20 : any;
1const foo = (): string => {2 return 'sabertaz';3};45// string6type FooReturnType = ReturnType<typeof foo>;
Type Guards
Discriminated Union Type Guard
1interface Teacher {2 kind: 'Teacher';3 teacherId: string;4}56interface Student {7 kind: 'Student';8 studentId: string;9}1011type Attendee = Teacher | Student;1213function getId(attendee: Attendee) {14 switch (attendee.kind) {15 case 'Teacher':16 // %inferred-type: { kind: "Teacher"; teacherId: string; }17 return attendee.teacherId;18 case 'Student':19 // %inferred-type: { kind: "Student"; studentId: string; }20 return attendee.studentId;21 default:22 throw new Error('Unsupported type');23 }24}
Never Type Guard
- The
never
type is assignable to every type. - No type is assignable to
never
(exceptnever
itself).
1interface Triangle {2 kind: 'triangle';3 sideLength: number;4}56type Shape = Circle | Square | Triangle;78function getArea(shape: Shape) {9 switch (shape.kind) {10 case 'circle':11 return Math.PI * shape.radius ** 2;12 case 'square':13 return shape.sideLength ** 2;14 default: {15 // Type 'Triangle' is not assignable to type 'never'.16 const _exhaustiveCheck: never = shape;17 return _exhaustiveCheck;18 }19 }20}
Exhaustiveness checks:
1class UnsupportedValueError extends Error {2 constructor(value: never) {3 super(`Unsupported value: ${value}`);4 }5}67function toGerman4(value: NoYesStrings): string {8 switch (value) {9 case 'Yes':10 return 'Ja';11 default:12 // @ts-expect-error: Argument of type '"No"'13 // is not assignable to parameter of type 'never'. (2345)14 throw new UnsupportedValueError(value);15 }16}
Type Predicates
is
keyword for value
type predicate:1type Falsy = false | '' | 0 | null | undefined;2const isFalsy = (val: unknown): val is Falsy => !val;
1function isNotNullish<T>(value: T): value is NonNullable<T> {2 return value !== undefined && value !== null;3}45// %inferred-type: (number | null | undefined)[]6const mixedValues = [1, undefined, 2, null];78// %inferred-type: number[]9const numbers = mixedValues.filter(isNotNullish);
1/**2 * A partial implementation of the `typeof` operator.3 */4function isTypeof(value: any, typeString: 'boolean'): value is boolean;5function isTypeof(value: any, typeString: 'number'): value is number;6function isTypeof(value: any, typeString: 'string'): value is string;7function isTypeof(value: any, typeString: string): boolean {8 return typeof value === typeString;9}1011const value: unknown = {};1213if (isTypeof(value, 'boolean')) {14 // %inferred-type: boolean15 console.log(value);16}
Type Assertion
<type>
.as type
.
as
is better in.jsx
1let foo: any;2const bar = foo as string; // 现在 bar 的类型是 'string'
1function handler(event: Event) {2 const mouseEvent = event as MouseEvent;3}
Type System
TypeScript type system:
- Turing complete type system.
- Structural type system: type checking focuses on shape (
Duck Typing
).
Covariant
Covariant (协变性):
Type
T
is covariant if having S <: P
,
then T<S> <: T<P>
.1type IsSubtype<S, P> = S extends P ? true : false;23type T1 = IsSubtype<Admin, User>;4// type T1 = true56type T2 = IsSubtype<Promise<Admin>, Promise<User>>;7// type T2 = true89type T3 = IsSubtype<'Hello', string>;10// type T3 = true1112type T4 = IsSubtype<Capitalize<'Hello'>, Capitalize<string>>;13// type T4 = true
Contravariant
Contravariant (逆变性):
Type
T
is contravariant if having S <: P
,
then T<P> <: T<S>
.1type IsSubtype<S, P> = S extends P ? true : false;23type Func<Param> = (param: Param) => void;45type T1 = IsSubtype<Admin, User>;6// type T1 = true78type T2 = IsSubtype<Func<Admin>, Func<User>>;9// type T2 = false1011type T3 = IsSubtype<Func<User>, Func<Admin>>;12// type T3 = true
1const logAdmin: Func<Admin> = (admin: Admin): void => {2 console.log(`Name: ${admin.userName}`);3 console.log(`Is super admin: ${admin.isSuperAdmin.toString()}`);4};56const logUser: Func<User> = (user: User): void => {7 console.log(`Name: ${user.userName}`);8};910const admin = new Admin('admin1', true);1112let logger: Func<Admin>;1314logger = logUser;15logger(admin); // OK1617logger = logAdmin;18logger(admin); // OK1920const user = new User('user1');2122let logger: Func<User>;2324logger = logUser;25logger(user); // OK2627logger = logAdmin;28// Type 'Func<Admin>' is not assignable to type 'Func<User>'.29// Property 'isSuperAdmin' is missing in type 'User' but required in type 'Admin'.30logger(user); // Oops! `user.isSuperAdmin` is undefined.
Type Gymnastics
Level | Environment | Operands | Operations |
---|---|---|---|
Program level | Runtime | Values | Functions |
Type level | Compile time | Specific types | Generic types |