TypeScript 高级类型与工具类型全解

本文献给:

已掌握 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)](#一、映射类型(Mapped Types))
    • [1.1 基本语法](#1.1 基本语法)
    • [1.2 内置映射类型](#1.2 内置映射类型)
    • [1.3 映射修饰符](#1.3 映射修饰符)
    • [1.4 键名重映射(as 子句,TypeScript 4.1+)](#1.4 键名重映射(as 子句,TypeScript 4.1+))
  • 二、实用工具类型详解
    • [2.1 Pick<T, K>](#2.1 Pick<T, K>)
    • [2.2 Omit<T, K>](#2.2 Omit<T, K>)
    • [2.3 Record<K, T>](#2.3 Record<K, T>)
    • [2.4 Exclude<T, U>](#2.4 Exclude<T, U>)
    • [2.5 Extract<T, U>](#2.5 Extract<T, U>)
    • [2.6 其他常用工具类型](#2.6 其他常用工具类型)
  • [三、条件类型(Conditional Types)](#三、条件类型(Conditional Types))
    • [3.1 基本用法](#3.1 基本用法)
    • [3.2 分布式条件类型](#3.2 分布式条件类型)
    • [3.3 内置的 `NonNullable` 实现](#3.3 内置的 NonNullable 实现)
    • [3.4 条件类型与泛型约束的区别](#3.4 条件类型与泛型约束的区别)
  • [四、infer 关键字 ------ 从类型中提取部分信息](#四、infer 关键字 —— 从类型中提取部分信息)
    • [4.1 提取函数返回值类型](#4.1 提取函数返回值类型)
    • [4.2 提取函数参数类型](#4.2 提取函数参数类型)
    • [4.3 提取数组元素类型](#4.3 提取数组元素类型)
    • [4.4 提取 Promise 内部类型](#4.4 提取 Promise 内部类型)
    • [4.5 多 infer 位置](#4.5 多 infer 位置)
  • [五、模板字面量类型(Template Literal Types)](#五、模板字面量类型(Template Literal Types))
    • [5.1 基本语法](#5.1 基本语法)
    • [5.2 内置字符串操作类型](#5.2 内置字符串操作类型)
    • [5.3 与映射类型结合](#5.3 与映射类型结合)
    • [5.4 用于事件监听器类型](#5.4 用于事件监听器类型)
  • [六、satisfies 运算符 ------ 类型保留的新方式](#六、satisfies 运算符 —— 类型保留的新方式)
    • [6.1 与类型断言的区别](#6.1 与类型断言的区别)
    • [6.2 常见场景:对象字面量](#6.2 常见场景:对象字面量)
  • [七、类型守卫高级模式 ------ is 与 asserts 深入](#七、类型守卫高级模式 —— is 与 asserts 深入)
    • [7.1 自定义类型守卫(is)](#7.1 自定义类型守卫(is))
    • [7.2 进阶:泛型守卫与类型谓词](#7.2 进阶:泛型守卫与类型谓词)
    • [7.3 断言函数(asserts)](#7.3 断言函数(asserts))
    • [7.4 简单断言(asserts condition)](#7.4 简单断言(asserts condition))
    • [7.5 断言函数 vs 类型守卫](#7.5 断言函数 vs 类型守卫)
  • 八、类型编程实战
    • [8.1 DeepReadonly](#8.1 DeepReadonly)
    • [8.2 DeepPartial](#8.2 DeepPartial)
    • [8.3 类型安全的 EventBus](#8.3 类型安全的 EventBus)
  • 九、小结

一、映射类型(Mapped Types)

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

1.1 基本语法

typescript 复制代码
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 的对象类型。
typescript 复制代码
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? 修饰符。默认是 +

typescript 复制代码
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 子句重新映射键名。

typescript 复制代码
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 构成新类型。

typescript 复制代码
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 相反。

typescript 复制代码
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。

typescript 复制代码
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 的成员。

typescript 复制代码
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 的成员。

typescript 复制代码
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>:获取构造函数实例类型。

示例:

typescript 复制代码
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 基本用法

typescript 复制代码
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;  // true
type B = IsString<number>;  // false

3.2 分布式条件类型

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

typescript 复制代码
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 实现

typescript 复制代码
type NonNullable<T> = T extends null | undefined ? never : T;
type T = NonNullable<string | null | undefined>; // string

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

  • 泛型约束(extends)限制可传入的类型范围。
  • 条件类型根据传入类型计算出新类型。
typescript 复制代码
// 约束:只能传有 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 提取函数返回值类型

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

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

4.2 提取函数参数类型

typescript 复制代码
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

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

4.3 提取数组元素类型

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

4.4 提取 Promise 内部类型

typescript 复制代码
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type R = UnwrapPromise<Promise<Promise<number>>>; // number

4.5 多 infer 位置

可以同时推断多个类型。

typescript 复制代码
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 基本语法

typescript 复制代码
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>:首字母小写。
typescript 复制代码
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"typescript">; // "Typescript"

5.3 与映射类型结合

typescript 复制代码
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 用于事件监听器类型

typescript 复制代码
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 与类型断言的区别

typescript 复制代码
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 常见场景:对象字面量

typescript 复制代码
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 不会拓宽,而是推断字面量

更典型的例子:

typescript 复制代码
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 作为返回类型。

typescript 复制代码
function isString(value: unknown): value is string {
    return typeof value === "string";
}

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

typescript 复制代码
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)

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

typescript 复制代码
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)

typescript 复制代码
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,让嵌套对象的所有属性(包括属性中的对象)都变为只读。

typescript 复制代码
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

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

typescript 复制代码
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

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

typescript 复制代码
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 可选。

优化版本:

typescript 复制代码
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 #类型守卫 #类型编程 #学习笔记 #前端开发

相关推荐
之歆12 小时前
Day22_CSS 函数完全指南:从变量到数学计算的现代样式编程
开发语言·前端·javascript·css·tensorflow·less
ZengLiangYi12 小时前
Prompt 工程:让 LLM 输出结构化 JSON
前端·javascript·后端
米丘12 小时前
React19.x 一个示例来看 Diff 算法
javascript·react.js
zithern_juejin12 小时前
手写instanceof
javascript
xiaobobo333013 小时前
Ubuntu经常安装软件
ubuntu·常用软件
ZengLiangYi13 小时前
MCP 协议从零实现:手写最简 MCP Server
前端·javascript·后端
yspwf13 小时前
Node.js 本地下载并使用 Hugging Face 中文向量模型:以 bge-base-zh-v1.5 为例
javascript·后端
Mr.Hazyzhao13 小时前
Ubuntu26.04 使用 nomachine 9.5.7 时黑屏,及使用 Rustdesk 时必须选择分享屏幕 的解决
ubuntu
小救星小杜、13 小时前
new Router base的作用
前端·javascript·vue.js