高级类型操作

用类型编程,让类型系统为你工作

🎯 学习目标

学完本章,你将能够:

  • 能够使用 keyof 和索引访问类型动态查询对象属性
  • 能够运用映射类型批量转换接口属性
  • 能够理解条件类型(extends ? :)与分布式条件类型
  • 能够使用 infer 在条件类型中提取和推断子类型
  • 能够运用模板字面量类型和 as 键重映射实现字符串类型操作

先看一个问题

如何从一个接口中提取所有属性名?如何让所有属性变成可选?如何自动推导 getter 的命名?

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

// 想要:提取属性名 → "name" | "age" | "email"
// 想要:全部变可选 → { name?: string; age?: number; email?: string }
// 想要:自动生成 getter → { getName: () => string; getAge: () => number }

这些需求用"值"的编程思维很难实现。TypeScript 提供了一套类型级别的操作工具:keyof、映射类型、条件类型、模板字面量类型。它们组合起来,就是"类型编程"。

Part 1: keyof

获取类型的所有属性名,生成联合类型

第25章我们用 keyof 做过简单的属性访问。现在来正式认识它——keyof T 能把一个接口的所有属性名提取成联合类型。配合索引访问 T[K],你就能在编译期保证属性名不会拼错。这是整个"类型编程"的起点。

keyof 索引查询

把类型的属性名提取出来

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

type UserKeys = keyof User; // "name" | "age" | "email"

// 索引访问类型 T[K] —— 获取某个属性的类型
type UserName = User["name"];  // string
type UserAge = User["age"];    // number

// 获取所有值的类型
type UserValues = User[keyof User]; // string | number
// 实际应用:安全的属性访问函数
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user: User = { name: "张三", age: 25, email: "a@b.c" };
const name = getProp(user, "name"); // string
const age = getProp(user, "age");   // number
// getProp(user, "phone");          // 报错!"phone" 不存在

嵌套索引访问

深层属性类型也能提取

// 数组的索引访问
type StringArray = string[];
type Item = StringArray[number]; // string

// 嵌套索引访问
interface Config {
    database: {
        host: string;
        port: number;
    };
    cache: {
        ttl: number;
        maxSize: number;
    };
}

type DbHost = Config["database"]["host"]; // string
type CacheConfig = Config["cache"];
// { ttl: number; maxSize: number }

就像 JS 中用 obj["a"]["b"] 访问嵌套属性一样,类型中也可以链式索引访问。

Part 2: 映射类型

基于旧类型生成新类型——对每个属性做变换

有了 keyof,你已经能拿到属性名了。映射类型更进一步——它像 for 循环一样遍历每个属性,对值类型做变换。想让所有属性变可选?变只读?变 string?一行映射类型搞定。前面第25章用过的 Partial、Readonly,其实就是映射类型的语法糖。

映射类型基础

遍历所有属性,逐一变换

// 让所有属性可选
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// 让所有属性只读
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// 让所有属性可空
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

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

type PartialUser = MyPartial<User>;
// { name?: string; age?: number }

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

映射类型进阶

变换值类型和从联合生成对象

// 变换值类型:所有属性都变成 string
type Stringify<T> = {
    [K in keyof T]: string;
};

interface User { name: string; age: number; }
type StringUser = Stringify<User>;
// { name: string; age: string }
// 从联合类型生成对象
type Flags = "hot" | "new" | "sale";
type FeatureFlags = {
    [K in Flags]: boolean;
};
// { hot: boolean; new: boolean; sale: boolean }

映射类型的语法 [K in keyof T] 可以理解为一个 for 循环——遍历 T 的每个属性 K,然后对值类型 T[K] 做变换。

Part 3: 条件类型

类型层面的 if-else,根据条件选择类型

映射类型能遍历属性,但它没法做判断——"如果这个属性是函数,就保留,否则去掉"这种逻辑怎么办?条件类型就是类型层面的 if-else。配合 infer 关键字,还能在匹配的过程中"顺手"提取子类型,像正则的捕获组一样。

条件类型

T extends U ? X : Y —— 类型的 if-else

// 基本条件类型
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;  // "yes"
type B = IsString<42>;        // "no"
type C = IsString<string | number>; // "yes" | "no"(分布式)
// 实际应用:类型守卫
type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T1 = TypeName<"hello">;  // "string"
type T2 = TypeName<42>;       // "number"
type T3 = TypeName<() => void>; // "function"

infer 推断

在条件类型中"捕获"子类型

// 提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = MyReturnType<() => string>;       // string
type FnReturn2 = MyReturnType<(x: number) => boolean>; // boolean

// 提取函数参数类型
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type FnParams = MyParameters<(x: string, y: number) => void>;
// [x: string, y: number]
// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<number>;           // number

// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string

infer 就像正则表达式的"捕获组"——在匹配过程中把某个部分提取出来。

分布式条件类型

联合类型会自动"展开"

// 当 T 是联合类型时,条件类型会自动分发
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 等价于 ToArray<string> | ToArray<number>
// 结果: string[] | number[]

// 阻止分发:用 [T] 包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNoDistribute<string | number>;
// 结果: (string | number)[]
注意区别:string[] | number[] 是"字符串数组或数字数组",(string | number)[] 是"元素可以是字符串或数字的数组"。分布式行为会影响结果类型。
图:分布式条件类型的展开过程

never 类型过滤

分布式条件类型 + never = 强力过滤器

// 从联合类型中过滤掉不需要的成员
type OnlyStrings<T> = T extends string ? T : never;

type Result = OnlyStrings<"hello" | 42 | "world" | true>;
// "hello" | "world" —— 数字和布尔值被 never 过滤掉了
// 手写 Extract 和 Exclude
type MyExtract<T, U> = T extends U ? T : never;
type MyExclude<T, U> = T extends U ? never : T;

type T1 = MyExtract<"a" | "b" | 1 | 2, string>;  // "a" | "b"
type T2 = MyExclude<"a" | "b" | 1 | 2, string>;  // 1 | 2

原理:分布式条件类型把联合类型逐个检查,不满足条件的变成 never。而 never 在联合类型中会被自动消除,就像 0 在加法中被忽略一样——不过这个类比只适用于联合类型,在元组和对象中 never 不会自动消失。

提取方法名

用 never 过滤出对象中所有的方法

// 提取 T 中所有方法名
type GetMethods<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];

interface UserService {
    name: string;
    version: number;
    getUser(): { id: string };
    updateUser(id: string): void;
    deleteUser(id: string): boolean;
}

type Methods = GetMethods<UserService>;
// "getUser" | "updateUser" | "deleteUser"
// 同理:提取所有非函数属性名
type GetDataProps<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
}[keyof T];

type DataProps = GetDataProps<UserService>;
// "name" | "version"
常见错误:忘记 [keyof T] 后缀。映射类型产生的是对象类型 { name: never; getUser: "getUser"; ... },需要用索引访问 [keyof T] 把值提取出来,never 才会被自动过滤。

Part 4: 模板字面量

在类型层面拼接字符串,自动生成命名模式

你写 React 时有没有想过,onClick、onFocus 这些事件名能不能自动推导?模板字面量类型就能做到。它可以在类型层面拼接字符串,配合 Capitalize 等内置工具,自动生成 getName、onClick 这样的命名模式。

模板字面量类型

类型层面的字符串拼接

// 自动生成事件处理器命名
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// 内置字符串操作类型
type Upper = Uppercase<"hello">;    // "HELLO"
type Lower = Lowercase<"HELLO">;    // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"
// CSS 属性类型
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"

// 动态事件类型
type DomainEvent<T extends string> = `${T}Changed` | `${T}Deleted`;
type UserEvent = DomainEvent<"user">; // "userChanged" | "userDeleted"

Part 5: 键重映射

用 as 重命名或过滤映射类型的键

映射类型能变换属性值,但属性名本身呢?键重映射用 as 关键字实现了这个能力——你可以给属性加前缀、改命名,甚至把不需要的属性过滤掉。前几节学的映射类型加上键重映射,就是完整的"类型编程工具箱"了。

as 键重映射

重命名键或过滤掉不需要的属性

// 重命名键:给所有属性加前缀
type Prefixed<T, P extends string> = {
    [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface Config { host: string; port: number; }
type DbConfig = Prefixed<Config, "db">;
// { dbHost: string; dbPort: number }
// 过滤键:只保留函数类型的属性
type FunctionProperties<T> = {
    [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

interface Mix {
    name: string;
    greet(): void;
    age: number;
    calc(x: number): number;
}
type OnlyFns = FunctionProperties<Mix>;
// { greet: () => void; calc: (x: number) => number }

综合实战

自动生成 Getter 类型

// 映射类型 + 模板字面量 + 键重映射
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

// 事件处理器映射
type EventHandlers<T extends Record<string, any>> = {
    [K in keyof T as `on${Capitalize<string & K>}`]: (data: T[K]) => void;
};

type Events = {
    click: { x: number; y: number };
    keypress: { key: string };
};

type Handlers = EventHandlers<Events>;
// { onClick: (data: { x: number; y: number }) => void;
//   onKeypress: (data: { key: string }) => void }

高级类型操作不必一次全部掌握。从实际需求出发,遇到问题再查对应的工具类型即可。

字符串解析模式

用模板字面量 + infer 解析字符串结构

// 从路由字符串中提取参数名
type ParseRoute<S extends string> =
    S extends `${string}:${infer Param}/${infer Rest}`
        ? Param | ParseRoute<Rest>
        : S extends `${string}:${infer Param}`
            ? Param
            : never;

type Params = ParseRoute<"/users/:userId/posts/:postId">;
// "userId" | "postId"

// 类型安全的路由定义
type RouteParams<T extends string> = ParseRoute<T>;

function createRoute<T extends string>(path: T) {
    return {
        path,
        build: (params: Record<RouteParams<T>, string>) => {
            /* ... */
        }
    };
}

const userPostRoute = createRoute("/users/:userId/posts/:postId");
// build 的参数类型: Record<"userId" | "postId", string>

模板字面量类型 + infer 就像类型层面的正则表达式。你可以在 extends 的右侧写模式串,用 infer 捕获"捕获组"。不过它不支持量词和复杂语法,只适合结构简单的字符串。

更多解析模式

CSS 属性名变换、事件名提取

// CSS 属性名转换:kebab-case → camelCase
type KebabToCamel<S extends string> =
    S extends `${infer Head}-${infer Tail}`
        ? `${Head}${Capitalize<KebabToCamel<Tail>>}`
        : S;

type CSS = KebabToCamel<"background-color">;    // "backgroundColor"
type CSS2 = KebabToCamel<"border-top-left-radius">; // "borderTopLeftRadius"
// 提取事件处理器中的事件名
type ExtractEventName<T extends string> =
    T extends `on${infer Event}` ? Uncapitalize<Event> : never;

type Click = ExtractEventName<"onClick">;    // "click"
type Focus = ExtractEventName<"onFocus">;    // "focus"
type NotEvent = ExtractEventName<"onClickHandler">; // "clickHandler"
注意:模板字面量中的 infer 是"贪婪匹配"还是"非贪婪匹配"取决于位置。在上面的 on${infer Event} 中,Event 会匹配到字符串末尾(贪婪)。如果需要精确匹配,要在模式中写清楚边界。

五大操作对比

一张表总结高级类型操作

操作语法作用
keyofkeyof T提取所有属性名为联合类型
索引访问T[K]获取某个属性的类型
映射类型[K in keyof T]遍历属性生成新类型
条件类型T extends U ? X : Y根据条件选择类型
模板字面量`${A}${B}`拼接字符串类型

递归类型

DeepPartial、DeepReadonly——类型也能递归

内置的 Partial<T> 只做一层。嵌套对象呢?需要自己写递归类型。

// 内置 Partial 只处理第一层
interface Config {
    db: { host: string; port: number };
    cache: { ttl: number };
}
type P = Partial<Config>;
// { db?: { host: string; port: number }; cache?: { ttl: number } }
// db 里面的属性并没有变可选!
// DeepPartial:递归地把所有属性变可选
type DeepPartial<T> = {
    [K in keyof T]?: T[K] extends object
        ? DeepPartial<T[K]>
        : T[K];
};

type DP = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; cache?: { ttl?: number } }

// DeepReadonly:递归地把所有属性变只读
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? DeepReadonly<T[K]>
        : T[K];
};

递归类型的套路是固定的:用条件类型判断属性值是否是对象,如果是就递归处理,否则直接返回。

映射变换全景

原接口 → 遍历属性 → 变换 → 新接口

映射类型本质上就是一个"类型的 for 循环"。让我们用 Partial 举例,看看变换的每一步:

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

// MyPartial 的变换过程:
// 第1步:keyof User → "name" | "age" | "email"
// 第2步:遍历每个属性 K
//   K = "name" → name?: string
//   K = "age"  → age?: number
//   K = "email" → email?: string
// 第3步:合并成新类型
type Result = { name?: string; age?: number; email?: string; }
图:映射类型的变换流程

映射类型的强大之处在于它是一个纯函数——输入一个类型,输出一个新类型,原类型不变。

协变与逆变

函数参数类型的兼容性规则

这个问题在写泛型回调时经常碰到。先看一个"反直觉"的例子:

// 疑问:下面的赋值安全吗?
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;

let handler: AnimalHandler = (a) => console.log(a.name);
// 把 AnimalHandler 赋给 DogHandler?
let dogHandler: DogHandler = handler; // 严格模式下报错!
// 协变(Covariance):方向一致
// Dog 是 Animal 的子类型 → Dog[] 也是 Animal[] 的子类型
// 返回值类型是协变的

// 逆变(Contravariance):方向相反
// 函数参数类型是逆变的:
// (animal: Animal) => void 是 (dog: Dog) => void 的子类型
// 因为能处理所有 Animal 的函数,一定能处理 Dog

协变(同方向)

子类型关系保持一致。Dog extends Animal,所以 Promise<Dog> 可以赋给 Promise<Animal>。

逆变(反方向)

子类型关系反转。(x: Animal) => void 可以赋给 (x: Dog) => void。参数越宽泛,函数越通用。

日常开发中不用深究,但遇到泛型回调的类型报错时,想想协变和逆变就能找到原因。

图:协变与逆变的方向性

协变实战指南

生产者场景:子类型关系保持一致

class Animal { name = "动物"; }
class Dog extends Animal { bark() { return "汪"; } }
class Cat extends Animal { meow() { return "喵"; } }

// 协变:Dog[] 可以赋给 Animal[]
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK — 数组是协变的
// 因为数组是"生产者"——你从中取出元素,元素至少是 Animal

// 反过来就不行
const mixedAnimals: Animal[] = [new Dog(), new Cat()];
// const dogList: Dog[] = mixedAnimals; // 报错!可能有 Cat
// Promise 也是协变的
async function getDog(): Promise<Dog> { return new Dog(); }

async function useAnimal(): Promise<Animal> {
    return getDog(); // OK — Promise<Dog> 赋给 Promise<Animal>
}

// 返回值也是协变:返回 Dog 的函数可以代替返回 Animal 的函数
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;

const makeDog: DogFactory = () => new Dog();
const makeAnimal: AnimalFactory = makeDog; // OK

类比记忆:"生产者协变"——如果某类型是"往外产出"数据的(数组元素、Promise 值、函数返回值),子类型关系保持一致方向。但这个类比不完美——TypeScript 中数组的 push 方法其实是"消费者",所以严格来说数组并不纯粹是生产者。

逆变实战指南

消费者场景:子类型关系反转

type Handler<T> = (event: T) => void;

// 能处理 Animal 的函数,一定能处理 Dog
const animalHandler: Handler<Animal> = (a) => {
    console.log(a.name); // 只用了 Animal 的属性
};

const dogHandler: Handler<Dog> = animalHandler; // OK (strictFunctionTypes)
// 把 Dog 传给 animalHandler,只访问 name —— 安全!

// 反过来就危险了:
// const badHandler: Handler<Animal> = (d: Dog) => d.bark();
// badHandler(new Cat()); // 运行时崩溃!Cat 没有 bark()
strictFunctionTypes 配置:只有在 tsconfig.json 中开启 "strictFunctionTypes": true(包含在 strict 模式中),函数参数才会正确地做逆变检查。关闭此选项时,函数参数会做双向协变——方便但不安全。

事件系统实战

协变 + 逆变在真实项目中的应用

// 类型安全的事件系统
interface EventMap {
    click: { x: number; y: number };
    keypress: { key: string; ctrl: boolean };
    resize: { width: number; height: number };
}

class TypeSafeEventEmitter<T extends Record<string, any>> {
    // Handler<T[K]> 是逆变的 —— 需要"宽泛"的处理器
    private handlers: { [K in keyof T]?: Array<(data: T[K]) => void> } = {};

    on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
        (this.handlers[event] ??= []).push(handler);
    }

    emit<K extends keyof T>(event: K, data: T[K]) {
        this.handlers[event]?.forEach(h => h(data));
    }
}

const emitter = new TypeSafeEventEmitter<EventMap>();
emitter.on("click", (data) => {
    console.log(data.x, data.y); // data 自动推导为 { x: number; y: number }
});
// emitter.on("click", (data) => data.key); // 报错!click 没有 key
// emitter.emit("keypress", { x: 1, y: 2 }); // 报错!数据类型不匹配

协变逆变的核心原则:"生产者协变,消费者逆变"。记住这句话,遇到泛型回调的类型报错就不慌了。

递归 Omit

练习

任务:实现一个 DeepOmit<T, K> 类型,递归地从嵌套对象中排除所有名为 K 的属性。

interface Config {
    id: string;
    db: { id: string; host: string; port: number };
    cache: { id: string; ttl: number };
}

type Clean = DeepOmit<Config, "id">;
// 期望结果:
// { db: { host: string; port: number }; cache: { ttl: number } }
// 所有层级的 id 都被移除
// 你的代码:

参考答案

type DeepOmit<T, K extends string> = {
    [P in keyof T as P extends K ? never : P]:
        T[P] extends object
            ? DeepOmit<T[P], K>
            : T[P];
};

// 验证
interface Config {
    id: string;
    db: { id: string; host: string; port: number };
    cache: { id: string; ttl: number };
}

type Clean = DeepOmit<Config, "id">;
// { db: { host: string; port: number }; cache: { ttl: number } }

常见问题

问题一:映射类型中 keyof 报错

// 映射类型只能用于类型,不能用于值
type Bad = { [K in keyof obj] }; // 报错!obj 是值不是类型
type Good = { [K in keyof User] }; // OK,User 是类型

问题二:infer 只能在条件类型中使用

// 报错:infer 不能出现在条件类型之外
type Bad<T> = infer R; // 报错!

// 正确:infer 在 extends 右侧
type Good<T> = T extends Promise<infer R> ? R : T;

问题三:Capitalize 对非 string 类型报错

// 需要用 string & K 做类型收窄
type Safe<K> = Capitalize<string & K>; // OK
type Unsafe<K> = Capitalize<K>;         // 可能报错

in 操作符收窄

运行时判断 + 类型层面的属性检测

// 运行时:in 操作符收窄联合类型
function processContact(contact: { email: string } | { phone: string }) {
    if ("email" in contact) {
        // TypeScript 自动收窄为 { email: string }
        console.log(contact.email); // OK
        // console.log(contact.phone); // 报错
    } else {
        // 自动收窄为 { phone: string }
        console.log(contact.phone); // OK
    }
}
// 类型层面:检测是否拥有某属性
type HasEmail<T> = T extends { email: string } ? true : false;

type A = HasEmail<{ name: string; email: string }>;  // true
type B = HasEmail<{ name: string; phone: string }>;  // false

// 检测可选属性(配合 & {} 技巧)
type IsOptional<T, K extends keyof T> =
    {} extends Pick<T, K> ? true : false;

interface User { name: string; age?: number; email: string; }
type OptAge = IsOptional<User, "age">;    // true
type OptName = IsOptional<User, "name">;  // false

in 和 keyof 的区别:keyof T 在类型层面提取所有属性名(编译时),"x" in obj 在运行时检查对象是否有某个属性。两者常配合使用:keyof 设计类型,in 做运行时守卫。

类型体操实战

组合 keyof + 条件 + 映射,手撕工具类型

// 挑战 1:DeepReadonly —— 递归只读
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? T[K] extends Function   // 函数不递归
            ? T[K]
            : DeepReadonly<T[K]>
        : T[K];
};

interface Config {
    db: { host: string; pool: { max: number } };
    log: () => void;
}
type Frozen = DeepReadonly<Config>;
// { readonly db: { readonly host: string; readonly pool: { readonly max: number } };
//   readonly log: () => void; }
// 挑战 2:RequiredKeys —— 提取所有必填属性的 key
type RequiredKeys<T> = {
    [K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];

interface User { id: string; name?: string; age: number; bio?: string; }
type RKeys = RequiredKeys<User>; // "id" | "age"

// 原理拆解:
// 1. [K in keyof T]-? 遍历所有属性并去掉 ?(不影响判断)
// 2. {} extends Pick<T, K> 检查空对象能否赋给只含 K 的类型
//    ——如果 K 是可选的,{} 就能赋过去,返回 never
//    ——如果 K 是必填的,赋不过去,返回 K
// 3. [keyof T] 把值取出来,never 被过滤

类型体操的解题套路:(1) 用 keyof 拿属性名;(2) 用映射类型遍历;(3) 用条件类型判断;(4) 用 never 过滤。掌握了这四步,大部分工具类型都能手写。

试一试

练习一

任务:手写一个 MyOmit<T, K> 类型,从 T 中排除 K 指定的属性。

// 提示:结合映射类型和 as 过滤
type MyOmit<T, K extends keyof T> = ???

interface User { id: string; name: string; password: string; }
type SafeUser = MyOmit<User, "password">;
// 期望:{ id: string; name: string }

参考答案

type MyOmit<T, K extends keyof T> = {
    [P in keyof T as P extends K ? never : P]: T[P];
};

练习二

任务:写一个 UnwrapPromise<T> 类型,递归解包嵌套的 Promise。

// UnwrapPromise<Promise<Promise<number>>>
// 应该得到 number
// 你的代码:

参考答案

type DeepUnwrap<T> =
    T extends Promise<infer U> ? DeepUnwrap<U> : T;

type Test = DeepUnwrap<Promise<Promise<Promise<number>>>>;
// number

📝 要点总结

本章核心知识点

keyof 与索引

keyof T 获取键联合,T[K] 索引访问,嵌套索引

映射类型

{ [K in keyof T]: ... } 批量转换属性

条件类型

T extends U ? X : Y,分布式条件类型自动分发

infer 推断

在条件类型中提取子类型,实现 ReturnType 等工具

模板字面量

类型级字符串操作,as 键重映射,递归类型

下一章

你已经掌握了类型编程的基础工具。下一章我们学习内置工具类型——TypeScript 自带的类型变换利器。

1 / X