本文献给:

已掌握 TypeScript 泛型、条件类型、索引访问类型等知识的开发者。本文将系统讲解 TypeScript 的高级类型特性,包括映射类型、内置工具类型(Pick、Omit、Record 等)、条件类型、infer 关键字、模板字面量类型、satisfies 运算符、高级类型守卫(isasserts),以及通过类型编程实战实现 DeepReadonlyDeepPartial 和类型安全的 EventBus,帮助你写出更精妙的类型代码。


你将学到:

  1. 映射类型的语法与内置映射类型(PartialReadonly 等)
  2. 常用工具类型:PickOmitRecordExcludeExtract
  3. 条件类型(T extends U ? X : Y)与分布式条件类型
  4. infer 关键字提取类型内部信息
  5. 模板字面量类型组合字符串类型
  6. satisfies 运算符保留具体类型同时满足约束
  7. 高级类型守卫:isasserts
  8. 类型编程实战:DeepReadonlyDeepPartial 与类型安全的 EventBus



一、映射类型(Mapped Types)

映射类型允许基于旧类型创建新类型,遍历旧类型的属性并应用转换。

1.1 基本语法

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type Partial<T> = {
    [P in keyof T]?: T[P];
};

interface User {
    name: string;
    age: number;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }

[P in keyof T] 遍历 T 的所有属性名,T[P] 获取属性类型。

1.2 内置映射类型

TypeScript 内置了常用的映射类型:

  • Partial<T>:所有属性变为可选。
  • Required<T>:所有属性变为必选(移除 ?)。
  • Readonly<T>:所有属性变为只读。
  • Pick<T, K>:选取部分属性(稍后详述)。
  • Record<K, T>:创建键为 K、值为 T 的对象类型。
type User = { name: string; age?: number };
type PartialUser = Partial<User>;       // { name?: string; age?: number }
type RequiredUser = Required<User>;     // { name: string; age: number }
type ReadonlyUser = Readonly<User>;     // { readonly name: string; readonly age?: number }

1.3 映射修饰符

+- 可以添加或移除 readonly? 修饰符。默认是 +

type RemoveReadonly<T> = {
    -readonly [P in keyof T]: T[P];
};

type MakeOptional<T> = {
    [P in keyof T]?: T[P];
};

type MakeRequired<T> = {
    [P in keyof T]-?: T[P];  // 移除可选修饰符
};

1.4 键名重映射(as 子句,TypeScript 4.1+)

可以使用 as 子句重新映射键名。

type Getters<T> = {
    [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person {
    name: string;
    age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

Capitalize 是内置模板字面量类型(后面会讲)。


二、实用工具类型详解

2.1 Pick<T, K>

从 T 中选取一组属性 K 构成新类型。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
// { title: string; completed: boolean; }

2.2 Omit<T, K>

从 T 中排除一组属性 K,与 Pick 相反。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type TodoWithoutDescription = Omit<Todo, "description">;
// { title: string; completed: boolean; }

2.3 Record<K, T>

创建一个对象类型,键为 K,值为 T。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

type UserMap = Record<string, { name: string }>;
const users: UserMap = {
    "user1": { name: "Alice" },
    "user2": { name: "Bob" }
};

type PageInfo = Record<"home" | "about" | "contact", { title: string }>;

2.4 Exclude<T, U>

从联合类型 T 中排除可赋值给 U 的成员。

type Exclude<T, U> = T extends U ? never : T;

type T = Exclude<"a" | "b" | "c", "a" | "b">; // "c"

2.5 Extract<T, U>

从联合类型 T 中提取可赋值给 U 的成员。

type Extract<T, U> = T extends U ? T : never;

type T = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"

2.6 其他常用工具类型

  • NonNullable<T>:排除 nullundefined(通过条件类型实现)。
  • ReturnType<T>:获取函数返回值类型(依赖 infer)。
  • Parameters<T>:获取函数参数类型元组。
  • ConstructorParameters<T>:获取构造函数参数类型。
  • InstanceType<T>:获取构造函数实例类型。

示例:

function greet(name: string): string {
    return `Hello, ${name}`;
}
type GreetReturn = ReturnType<typeof greet>; // string
type GreetParams = Parameters<typeof greet>; // [name: string]

class User {
    constructor(public id: number, public name: string) {}
}
type UserCtorParams = ConstructorParameters<typeof User>; // [id: number, name: string]
type UserInstance = InstanceType<typeof User>; // User

三、条件类型(Conditional Types)

条件类型的语法:T extends U ? X : Y,类似于三元表达式。

3.1 基本用法

type IsString<T> = T extends string ? true : false;
type A = IsString<string>;  // true
type B = IsString<number>;  // false

3.2 分布式条件类型

当 T 是一个联合类型时,条件类型会分布到每个成员上。

type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

// 如果想避免分布行为,可以用元组包裹
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDistributive<string | number>; // (string | number)[]

3.3 内置的 NonNullable 实现

type NonNullable<T> = T extends null | undefined ? never : T;
type T = NonNullable<string | null | undefined>; // string

3.4 条件类型与泛型约束的区别

  • 泛型约束(extends)限制可传入的类型范围。
  • 条件类型根据传入类型计算出新类型。
// 约束:只能传有 length 属性的类型
function logLen<T extends { length: number }>(arg: T) {}

// 条件类型:根据类型返回不同结果
type TypeName<T> = T extends string ? "string" :
                   T extends number ? "number" :
                   T extends boolean ? "boolean" : "other";

四、infer 关键字 —— 从类型中提取部分信息

infer 用于在条件类型中声明一个待推断的类型变量

4.1 提取函数返回值类型

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function fn() { return 42; }
type R = ReturnType<typeof fn>; // number

4.2 提取函数参数类型

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type Params = Parameters<(a: string, b: number) => void>; // [string, number]

4.3 提取数组元素类型

type ElementType<T> = T extends (infer U)[] ? U : never;
type E = ElementType<string[]>; // string

4.4 提取 Promise 内部类型

type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type R = UnwrapPromise<Promise<Promise<number>>>; // number

4.5 多 infer 位置

可以同时推断多个类型。

type Both<T> = T extends { a: infer A; b: infer B } ? [A, B] : never;
type R = Both<{ a: string; b: number }>; // [string, number]

五、模板字面量类型(Template Literal Types)

TypeScript 4.1 引入了模板字面量类型,允许在类型级别操作字符串。

5.1 基本语法

type Greeting = `Hello, ${string}`;
let g: Greeting = "Hello, world"; // OK
// g = "Hi there"; // ❌

type EventName<T extends string> = `${T}Changed`;
type ResizeEvent = EventName<"resize">; // "resizeChanged"

5.2 内置字符串操作类型

  • Uppercase<StringType>:将字符串转为大写。
  • Lowercase<StringType>:转小写。
  • Capitalize<StringType>:首字母大写。
  • Uncapitalize<StringType>:首字母小写。
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"typescript">; // "Typescript"

5.3 与映射类型结合

type Getters<T> = {
    [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
    name: string;
    age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

5.4 用于事件监听器类型

type EventNames = "click" | "focus" | "blur";
type EventHandlers = {
    [K in EventNames as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: (event: Event) => void; onFocus: ...; onBlur: ...; }

六、satisfies 运算符 —— 类型保留的新方式

satisfies 运算符(TypeScript 4.9+)用于检查一个表达式是否符合某个类型,同时保留表达式的具体类型,而不是拓宽为那个类型。

6.1 与类型断言的区别

type Colors = "red" | "green" | "blue";

// 断言:丢失了具体值信息,被收窄为 Colors
const color1 = "red" as Colors;        // 类型为 "red" 还是 Colors?实际是 Colors
const color2 = "red" satisfies Colors; // 类型为 "red",但仍然满足 Colors

// 实际效果:satisfies 保留字面量类型
const config = {
    theme: "dark",
    size: 100
} satisfies { theme: "dark" | "light"; size: number };

config.theme; // 类型为 "dark"(字面量),但可以赋值给 "dark"|"light"
// config.theme = "light"; // ❌ 类型 "dark" 不能赋给 "light",但实际不能修改?因为 config 是常量

6.2 常见场景:对象字面量

type Route = { path: string; children?: Route[] };

const routes = {
    home: { path: "/" },
    user: { path: "/user", children: [{ path: "/user/profile" }] }
} satisfies Record<string, Route>;

// routes.home.path 类型是 string(字面量"/"被拓宽为string?)
// 实际上 satisfies 不会拓宽,而是推断字面量

更典型的例子:

type Colors = "red" | "green" | "blue";

const colorSet = {
    primary: "red",
    secondary: "green"
} satisfies Record<string, Colors>;

colorSet.primary; // 类型为 "red"
// colorSet.primary = "blue"; // ❌ 只读?不是,但常量不可重新赋值

satisfies 解决了既要类型检查,又要保留字面量类型精确信息的需求。


七、类型守卫高级模式 —— is 与 asserts 深入

7.1 自定义类型守卫(is)

回顾基础:value is Type 作为返回类型。

function isString(value: unknown): value is string {
    return typeof value === "string";
}

7.2 进阶:泛型守卫与类型谓词

function isArrayOf<T>(value: unknown, check: (item: unknown) => item is T): value is T[] {
    return Array.isArray(value) && value.every(check);
}

const isNumber = (x: unknown): x is number => typeof x === "number";

const data: unknown = [1, 2, 3];
if (isArrayOf(data, isNumber)) {
    // data 类型为 number[]
    console.log(data.reduce((a,b)=>a+b,0));
}

7.3 断言函数(asserts)

断言函数不返回值,如果条件失败则抛出错误,从而在后续代码中收窄类型。

function assertIsString(value: unknown): asserts value is string {
    if (typeof value !== "string") {
        throw new Error("Not a string");
    }
}

function toUpper(value: unknown) {
    assertIsString(value);
    return value.toUpperCase(); // value 已收窄为 string
}

7.4 简单断言(asserts condition)

function assert(condition: any, msg?: string): asserts condition {
    if (!condition) throw new Error(msg ?? "Assertion failed");
}

function process(value: string | null) {
    assert(value !== null);
    // value 为 string
}

7.5 断言函数 vs 类型守卫

  • 守卫返回 boolean,用于 if 分支。
  • 断言失败抛出异常,后续代码无条件收窄。

选择:如果希望“一旦检查通过就假定为某类型,否则错误终止”,用断言;如果希望分支处理,用守卫。


八、类型编程实战

8.1 DeepReadonly

实现递归的 Readonly,让嵌套对象的所有属性(包括属性中的对象)都变为只读。

type Primitive = string | number | boolean | symbol | bigint | null | undefined;
type DeepReadonly<T> = T extends Primitive
    ? T
    : T extends Array<infer U>
    ? ReadonlyArray<DeepReadonly<U>>
    : T extends Map<infer K, infer V>
    ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
    : T extends Set<infer U>
    ? ReadonlySet<DeepReadonly<U>>
    : { readonly [P in keyof T]: DeepReadonly<T[P]> };

interface User {
    name: string;
    address: {
        city: string;
        zip: number;
    };
    tags: string[];
}

type ReadonlyUser = DeepReadonly<User>;
// 所有属性递归只读

8.2 DeepPartial

递归将属性变为可选,同样处理嵌套对象和数组。

type DeepPartial<T> = T extends Primitive
    ? T
    : T extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T extends Map<infer K, infer V>
    ? Map<DeepPartial<K>, DeepPartial<V>>
    : T extends Set<infer U>
    ? Set<DeepPartial<U>>
    : { [P in keyof T]?: DeepPartial<T[P]> };

8.3 类型安全的 EventBus

实现一个事件总线,事件名到回调参数类型的映射。

type EventMap = {
    login: { userId: number };
    logout: void;
    message: { text: string };
};

class EventBus<T extends Record<string, any>> {
    private listeners: {
        [K in keyof T]?: ((payload: T[K]) => void)[];
    } = {};
    
    on<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void {
        if (!this.listeners[event]) this.listeners[event] = [];
        this.listeners[event]!.push(callback);
    }
    
    emit<K extends keyof T>(event: K, payload: T[K]): void {
        const callbacks = this.listeners[event];
        if (callbacks) {
            callbacks.forEach(cb => cb(payload));
        }
    }
    
    off<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void {
        const callbacks = this.listeners[event];
        if (callbacks) {
            this.listeners[event] = callbacks.filter(cb => cb !== callback);
        }
    }
}

// 使用
const bus = new EventBus<EventMap>();
bus.on("login", (data) => {
    console.log(data.userId); // data 类型为 { userId: number }
});
bus.emit("login", { userId: 123 });
bus.emit("logout"); // void 类型,可以不传参数?实际上 payload 类型为 void,可以传 undefined 或不传

注意:void 在事件中可传 undefined 或不传,但为了类型安全,可以调整映射为 void 时 payload 可选。

优化版本:

type EventMap2 = {
    login: { userId: number };
    logout: undefined; // 表示不需要数据
    message: { text: string };
};

class EventBus2<T extends Record<string, any>> {
    // ... 类似实现,emit 时 payload 类型为 T[K] 即可
}
bus2.emit("logout", undefined);

九、小结

概念 关键语法 / 示例 说明
映射类型 { [P in K]: T[P] } 遍历属性修改类型
Partial / Readonly Partial<T>Readonly<T> 内置映射工具
Pick / Omit Pick<T, K>Omit<T, K> 选取或排除属性
Record Record<K, T> 构造键值对类型
Exclude / Extract 条件类型的分布应用 过滤联合类型成员
条件类型 T extends U ? X : Y 类型级别分支
infer infer R 提取类型变量
模板字面量类型 `${Uppercase<T>}` 字符串类型操作
satisfies expr satisfies Type 保留具体类型同时检查兼容性
断言函数 asserts value is Type 失败抛异常,收窄类型
类型编程实战 DeepReadonlyDeepPartialEventBus 递归映射 + 条件类型 + 泛型



觉得文章有帮助?别忘了:

👍 点赞 👍 – 给我一点鼓励
⭐ 收藏 ⭐ – 方便以后查看
🔔 关注 🔔 – 获取更新通知



标签: #TypeScript #高级类型 #工具类型 #条件类型 #infer #模板字面量 #satisfies #类型守卫 #类型编程 #学习笔记 #前端开发

更多推荐