TypeScript 笔记
最近整体梳理了一遍Typescript的一些语法和技巧,简以记录
type 与 interface 选型
// ---- type 更适合的场景 ----
// 字面联合
type Status = "idle" | "loading" | "success" | "error";
// 元组
type Point2D = [number, number];
// 函数类型
type AsyncFetch = (url: string) => Promise<Response>;
// 工具泛型
type Nullable<T> = T | null;
type ValueOf<T> = T[keyof T];
// ---- interface 更适合的场景 ----
// 业务实体
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
// 接口入参 / 出参
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
user: User;
}
// 类约束
interface Repository<T> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<void>;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
/* ... */
}
async save(user: User): Promise<void> {
/* ... */
}
}
// 声明合并:第三方库扩展
interface Window {
__INITIAL_STATE__: Record<string, unknown>;
}
// ---- 混合使用 ----
type Geo = { lat: number; lng: number };
interface Venue {
name: string;
location: Geo; // interface 字段复用 type
}
interface EnhancedVenue extends Venue, Geo {} // extends 复用 type
as const 提取联合类型
as const 将数组字面量收窄为只读元组,随后用索引访问 [number]["path"] 即可提取出所有路径的联合类型。
const ROUTES = [
{ path: "/", title: "首页", auth: false },
{ path: "/dashboard", title: "仪表盘", auth: true },
{ path: "/admin", title: "管理后台", auth: true },
] as const;
// typeof ROUTES[number]["path"] → "/" | "/dashboard" | "/admin"
type RoutePath = (typeof ROUTES)[number]["path"];
函数泛型中 extends 的两种语义
function getProp<T extends object, K extends PropertyKey, D>(
obj: T,
key: K,
defaultValue: D,
): K extends keyof T ? T[K] : D {
// ...
}
- 尖括号内
extends:泛型约束,要求T是object的子类型,K是PropertyKey的子类型。 - 返回值处
extends:条件类型,判断K是否为T的 key 之一。是则返回T[K],否则返回默认值类型D。
枚举
// 普通枚举:编译后生成对象,支持反向映射
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}
Color.Red; // 0
Color[0]; // "Red" -- 反向映射
Color[Color.Red]; // "Red"
// 指定值
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
// const enum:编译时直接内联,不生成对象的枚举可以使用
const enum Direction {
Up,
Down,
Left,
Right,
}
let dir = Direction.Up; // 编译后 → let dir = 0;
// Direction[0] // 报错,const enum 无反查
函数重载
同一个函数对多种入参有不同处理时,需声明多个重载签名,配合实现签名内的类型收窄:
function sum(num: number, ...rest: number[]): number;
function sum(nums: number[]): number;
function sum(first: number | number[], ...rest: number[]): number {
const numList = Array.isArray(first) ? first : [first, ...rest];
return numList.reduce((acc, cur) => acc + cur, 0);
}
重载签名对外暴露精确类型,实现签名使用更宽的联合类型做内部收窄。注意实现签名对外不可见。
联合类型收窄与 never 穷尽性检查
type Admin = { role: "admin"; permissions: string[]; level: number };
type Member = { role: "member"; nickname: string; joinedAt: Date };
type User = Admin | Member;
function printUserRole(user: User): string {
switch (user.role) {
case "admin":
return `管理员 Lv${user.level}: 拥有${user.permissions.length}项权限`;
case "member":
return `成员 ${user.nickname}: 加入于${user.joinedAt.toLocaleString()}`;
default: {
// 编译时检查:若新增 variant 但未处理,此处类型不再是 never,编译报错
const _exhaustive: never = user;
throw new Error(`未处理的 role 类型: ${_exhaustive}`);
}
}
}
default 分支中 user 被推断为 never,对其赋值可确保所有联合成员已被前面的分支穷尽处理。一旦新增成员而未补 case,此处会触发类型错误。
infer 模式匹配
infer 在条件类型的 extends 子句中声明一个类型变量,用于捕获匹配位置的具体类型:
type MyAwaited<T> = T extends Promise<infer V> ? V : never;
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any[]
) => infer R
? R
: never;
三个实例分别在内层 Promise、函数参数位、函数返回值位通过 infer 捕获对应类型。
Mapped Types 条件过滤与递归
按 key 过滤属性,never 即删除该属性,下为仅保留符合V类型的那些字段
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
递归深度处理,以 DeepPartial 为例:
type DeepPartial<T> =
T extends Array<infer U>
? Array<DeepPartial<U>> // 数组:递归处理元素
: T extends object
? T extends (...args: any[]) => any
? T // 函数:原样保留
: { [K in keyof T]?: DeepPartial<T[K]> } // 对象:递归处理每个属性
: T; // 原始类型:原样保留
看起来很吓人,实际上就三层:先看是不是数组,如果是就对每个元素递归处理;再看是不是函数,函数直接保留;最后如果是对象,递归对每一个属性处理; 停止条件就是基础类型,原样保留
内置工具类型手写实现
// 全部变成可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 全部必选
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
//全部只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
//保留指定key
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
//忽略指定key
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
//类型的for循环语法糖
type MyRecord<K extends PropertyKey, V> = {
[P in K]: V;
};
//联合类型排除
type MyExclude<T, U> = T extends U ? never : T;
//联合类型提取
type MyExtract<T, U> = T extends U ? T : never;
//联合类型去空
type MyNonNullable<T> = T extends null | undefined ? never : T;
//提取函数参数类型
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
//提取函数返回值类型
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any[]
) => infer R
? R
: never;
// 特殊讲解: 从构造函数类型提取实例类型
//
// 前置知识:class 声明同时产生两种类型 ——
// Dog → 实例类型,即 { name: string; bark(): void }
// typeof Dog → 构造函数类型,即 new () => Dog
//
// InstanceType 接收的是后者(带 new 签名的构造函数类型),
// 所以传参时必须写 InstanceType<typeof Dog>,而非 InstanceType<Dog>。
//
// new (...args: any) => infer R 中的 new 就是构造签名的标志:
// 它表示"可被 new 调用的函数,返回 R"。infer R 捕获 R 即得到实例类型。
//
// abstract new 用于兼容抽象类(不加则 typeof AbstractClass 不满足 new 约束)。
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
补充: 既然 InstanceType<typeof Dog> 返回的就是 Dog,直接写 Dog 不就行了?
答:能写出类名 Dog 时确实不需要;但泛型/高阶函数里手上只有一个构造函数类型变量 T,
无法直接引用类名,必须用 InstanceType<T> 反向解出实例类型:
举个例子:
function createInstance<T extends abstract new (...args: any) => any>(
Ctor: T,
): InstanceType<T> {
return new Ctor();
}
//
// 这里 T 是构造函数类型,不能当实例类型用,InstanceType<T> 就是它的"实例解包器"。
核心技巧只有三个:infer 捕获、as 过滤、extends 分发。
类泛型:分层约束
类级泛型与成员方法级泛型分别定义,在事件发布-订阅模式中尤为典型。Events 在实例化时确定整体结构,K 在每个方法调用时确定具体事件名:
type BaseEvent = Record<string, any[]>;
class EventEmitter<Events extends BaseEvent> {
private handlers = new Map<keyof Events, ((...args: any[]) => void)[]>();
on<K extends keyof Events>(
key: K,
handler: (...args: Events[K]) => void,
): void {
const list = this.handlers.get(key);
list ? list.push(handler) : this.handlers.set(key, [handler]);
}
emit<K extends keyof Events>(key: K, ...args: Events[K]): void {
this.handlers.get(key)?.forEach((fn) => fn(...args));
}
off<K extends keyof Events>(
key: K,
handler: (...args: Events[K]) => void,
): void {
const list = this.handlers.get(key);
if (list) {
this.handlers.set(
key,
list.filter((fn) => fn !== handler),
);
}
}
}
这样 emit 的参数类型完全由 Events[K] 推导,调用时每个事件获得精确的类型提示。