TypeScript进阶:类型体操与工程化实战

2026年 阅读约 18 分钟 面试指南 · TypeScript

TypeScript进阶指南:从泛型、条件类型、infer 到模板字面量类型体操,再到 tsconfig、声明文件、类型测试的工程化落地,附手写内置工具类型与高频面试题。

一句话总结

TypeScript 的进阶价值,是把"运行时的约束"提前到编译期:类型体操让你用类型本身表达业务逻辑的不变量(例如"这个函数的返回值一定是数组元素类型"),而工程化配置(strict / 声明文件 / 类型测试)则让大型项目在持续演化中始终保持类型安全。本篇不堆砌语法,而是从"为什么这样设计"出发,带你把基础篇没讲透的泛型、条件类型、infer、模板字面量,以及落地到团队的工程化手段串成一条线。

阅读建议:初级/中级偏理解,高级偏"类型即程序",实战偏团队落地。面试只考三分之一,但能写出高级章节的类型,基本能秒杀绝大多数 TS 进阶面。

初级:类型系统巩固

进阶题翻车,八成是基础语义没吃透。先补齐四个最容易混淆的点。

1. type 与 interface:什么时候用哪个

两者都能描述对象形状,但有本质差异:

  • interface 支持声明合并(多次声明自动合并),适合描述"对象/类契约",可被 class implements;
  • type 是类型别名,能做联合 A | B、交叉 A & B、映射类型、条件类型,灵活度更高,但不能声明合并。
// interface:可合并,适合公开 API 契约
interface User {
  id: number;
  name: string;
}
interface User {            // 合并:补充字段
  email: string;
}

// type:做组合更自然
type ID = number | string;
type Admin = User & { role: 'admin' };

经验法则:能用 interface 就用 interface(报错信息更友好、可被扩展);需要联合/交叉/工具类型时用 type。

2. 联合类型 vs 交叉类型

  • A | B 表示"或"——值可以是 A 或 B,访问属性时只能访问两者共有的;
  • A & B 表示"且"——值必须同时满足 A 和 B,属性合并;若同名属性类型冲突,结果变为 never。

3. 字面量类型与类型收窄(narrowing)

TS 能在 if 分支里把"宽类型"收窄成"窄类型"。常用收窄手段:=== 字面量比较、typeof、in、instanceof,以及判别联合(discriminated union)。

type Shape =
  | { kind: 'circle'; r: number }
  | { kind: 'rect'; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {        // 判别属性 kind 让 TS 精确收窄
    case 'circle': return Math.PI * s.r ** 2;
    case 'rect':   return s.w * s.h;
  }
}

4. 枚举 vs const 断言字面量联合

普通 enum 会生成运行时对象且可被反向映射,带来体积与语义成本;优先用 as const 字面量联合,零运行时成本、类型更精确。

// 不推荐:有运行时对象
enum Status { Ok = 1, Err = 2 }

// 推荐:纯类型,零运行时
const Status = { Ok: 1, Err: 2 } as const;
type Status = typeof Status[keyof typeof Status]; // 1 | 2

中级:泛型与映射/条件类型

泛型是"类型层面的函数",映射/条件类型是它的两个核心算子。

1. 泛型函数、约束与 keyof / 索引访问

function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];               // 返回值类型精确为 T[K]
}

interface User { id: number; name: string; age: number }
const u: User = { id: 1, name: 'a', age: 20 };
const n: number = pick(u, 'age');   // ✅ K 被约束为 'id'|'name'|'age'
// pick(u, 'email');              // ❌ 报错:'email' 不在 keyof User

keyof T 取所有键的联合,T[K] 做索引访问——这是所有工具类型的基石。

2. 映射类型(Mapped Types)

遍历一个类型的所有键,生成新类型:{ [K in keyof T]: T[K] }。内置 Partial/Required/Readonly/Record 都是它的语法糖。

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

3. 条件类型与分布式分发

T extends U ? X : Y 像三元表达式。当 T 是裸类型参数(未被数组/元组/Promise 等包裹)且为联合类型时,会自动分发到每个成员上。

type IsString<T> = T extends string ? true : false;
type R = IsString<string | number>;   // true | false(分发到两个成员)

若想关闭分发,用方括号包住 T:[T] extends [U] ? X : Y。

4. 手写 MyPick / MyReadonly(实战入门)

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

高级:类型体操实战

这一节的写法是"用类型写一个程序":输入一个类型,输出另一个类型。面试常考,工程中用于把数据库行类型精确投射到 DTO。

1. infer:在条件类型里"捕获"片段

infer 让你在 extends 的"模式"里挖个坑,把匹配到的类型片段存成变量。

type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<number[]>;        // number

type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type B = UnwrapPromise<Promise<string>>; // string

2. 递归条件类型

深度 Partial、元组转联合都靠递归。注意 TS 4.5+ 支持,且递归深度有上限。

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

type UnionFromTuple<T extends unknown[]> =
  T extends [infer First, ...infer Rest]
    ? First | UnionFromTuple<Rest>
    : never;

3. 模板字面量类型(Template Literal Types)

把字符串类型当模板拼装,配合 Capitalize/Uppercase 等内置工具,可生成"事件名""路由路径"这类派生字符串类型。

type EventName<K extends string> = `on${Capitalize<K>}`;
type Click = EventName<'click'>;     // "onClick"

type Path<T, K extends keyof T> = K extends string ? `GET /${K}` : never;

4. 手写内置工具类型全家桶

type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T;

5. this 类型与多态 this(链式调用)

class Builder {
  private items: string[] = [];
  add(x: string): this { this.items.push(x); return this; } // 返回 this 类型
}
class Sub extends Builder { tag() { return 'sub'; } }
new Sub().add('a').tag();   // ✅ 链式后仍保留 Sub 类型

实战:工程化落地

类型体操再花哨,落不到工程里都是空中楼阁。这一节讲团队真正用得上的配置与流程。

1. tsconfig 精要

  • "strict": true:开启 strictNullChecks/noImplicitAny/strictFunctionTypes 等一篮子检查,是类型安全的地基;
  • "moduleResolution": "bundler":配合 Vite/webpack5,支持 package.json exports 与省略扩展名;
  • "paths" + "baseUrl":路径别名 @/...,记得同步打包器与 tsconfig;
  • "verbatimModuleSyntax": true:强制区分 import type 与值导入,利于 tree-shaking 与隔离编译。

2. 声明文件 .d.ts 与 declare

给无类型的 JS、全局变量或第三方黑盒补类型,避免到处 any。

// legacy-lib.d.ts
declare module 'legacy-lib' {
  export function doThing(input: string): number;
}
declare const __APP_VERSION__: string;   // 全局注入的版本号

3. 与 ESLint / 打包器协作

用 typescript-eslint 统一规则(如 no-explicit-any、consistent-type-definitions),把"禁用 any""统一 type/interface 风格"写成 CI 门禁,而不是靠口头约定。

4. 类型测试:把类型当断言

公共类型/DTO 一旦改坏,下游全崩。用 vitest 的 expectTypeOf 或 tsd 写"类型单测"。

import { expectTypeOf } from 'vitest';
expectTypeOf(pick(u, 'age')).toEqualTypeOf<number>();
expectTypeOf(MyOmit<User, 'age'>).not.toEqualTypeOf<User>();

5. 渐进迁移策略

  • 纯 JS 仓库:加 "allowJs": true + "checkJs": true,再用 // @ts-check 逐文件开启;
  • Vue/Svelte:组件 <script lang="ts"> 渐进替换;
  • 老库无类型:局部 // @ts-expect-error 标注并登记 TODO,而非全局 any。

面试模拟

Q1:type 和 interface 怎么选?

公开对象契约、需要被类 implements 或可能扩展的用 interface;做联合/交叉/工具类型组合时用 type。两者大部分场景可互换,团队的 consistent-type-definitions 规则应统一一种为主。

Q2:什么是分布式条件类型?为什么联合会分发?

当条件类型的检查对象是裸类型参数且为联合时,TS 会把联合"拆开"逐成员计算再合并。用 [T] extends [U] 包裹可关闭分发,得到整体判断结果。

Q3:infer 解决了什么问题?手写取 Promise 值的类型。

infer 在模式匹配中捕获子类型。答案:type Awaited<T> = T extends PromiseLike<infer V> ? Awaited<V> : T;(递归处理嵌套 Promise)。

Q4:如何避免 any 扩散?unknown 和 any 的区别?

any 关闭所有检查、会"泄漏"到下游;unknown 是"类型安全版的 any",使用前必须收窄。策略:默认 unknown,开 noImplicitAny,对第三方黑盒用 .d.ts 收口。

Q5:strict 包含哪些关键检查?为什么建议全开?

核心有 strictNullChecks(消灭 null/undefined 空指针类错误)、noImplicitAny、strictFunctionTypes(函数参数逆变检查)、noImplicitThis 等。全开能在编译期拦截大量运行时崩溃。

Q6:类型体操在实际项目中该用到什么程度?

原则:让类型服务于业务不变量,而不是炫技。DTO 投射、判别联合建模、配置对象的精确类型是高 ROI 场景;过度递归/深层模板字面量会拖慢编译且难维护,应克制并用类型测试守护。