TypeScript 高級類型與實戰技巧 2026

🔷 類型是最好的文檔。 TypeScript 的高級類型系統是其最強大的特性之一。掌握條件類型、映射類型、模板字面量類型等"類型體操"技巧,可以讓你在編譯階段就消除大量 Bug,寫出更健壯的代碼。本文帶你從進階到精通 TypeScript 類型系統。
本文將帶你係統掌握:
- ✅ 條件類型與分佈式條件類型
- ✅ 映射類型與鍵重映射
- ✅ 模板字面量類型
- ✅ infer 關鍵字與類型提取
- ✅ 類型守衛與類型收窄
- ✅ 實用類型體操技巧
- ✅ 類型編程實戰案例
- ✅ 類型性能優化
一、條件類型
1.1 基本語法
條件類型就像類型層面的三元運算符:
T extends U ? X : Y示例:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // true1.2 實用條件類型
// 排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// 提取函數返回類型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 提取 Promise 返回類型
type Awaited<T> = T extends Promise<infer U> ? U : T;
// 提取數組元素類型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type R1 = ElementOf<string[]>; // string
type R2 = ElementOf<number[]>; // number
type R3 = Awaited<Promise<boolean>>; // boolean1.3 分佈式條件類型
當 T 是聯合類型時,條件類型會分佈式地應用於每個成員:
type ToArray<T> = T extends any ? T[] : never;
// 分佈式:聯合類型會被拆開
type R = ToArray<string | number>;
// string[] | number[]
// 阻止分佈:用方括號包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNonDist<string | number>;
// (string | number)[]實戰:排除類型
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"二、infer 關鍵字
2.1 基本用法
infer 用於在條件類型中聲明並推斷類型變量:
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = GetReturnType<() => string>; // string2.2 提取函數參數類型
// 獲取第一個參數類型
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
// 獲取所有參數類型
type Params<T> = T extends (...args: infer P) => any ? P : never;
type P1 = FirstParam<(name: string, age: number) => void>; // string
type P2 = Params<(name: string, age: number) => void>; // [string, number]2.3 提取構造函數類型
type InstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;
class Person {}
type P = InstanceType<typeof Person>; // Person2.4 提取數組首尾元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type F = First<[1, 2, 3]>; // 1
type L = Last<[1, 2, 3]>; // 32.5 提取 Promise 鏈
// 遞歸解包嵌套 Promise
type UnwrapPromise<T> = T extends Promise<infer U>
? U extends Promise<any>
? UnwrapPromise<U>
: U
: T;
type R = UnwrapPromise<Promise<Promise<Promise<number>>>>; // number三、映射類型
3.1 基本映射
映射類型可以基於舊類型創建新類型:
// 所有屬性變為只讀
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 所有屬性變為可選
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 所有屬性變為可空
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};3.2 鍵重映射(Key Remapping)
TypeScript 4.1+ 支持在映射類型中重新映射鍵:
// 將所有鍵添加前綴
type AddPrefix<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
};
interface User {
name: string;
age: number;
}
type PrefixedUser = AddPrefix<User, "user">;
// { userName: string; userAge: number }3.3 過濾屬性
// 只保留函數屬性
type FunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
// 只保留非函數屬性
type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
class MyClass {
name = "test";
age = 18;
greet() {}
}
type Data = NonFunctionProperties<MyClass>; // { name: string; age: number }3.4 修改屬性修飾符
// 移除 readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// 移除可選
type Required<T> = {
[P in keyof T]-?: T[P];
};四、模板字面量類型
4.1 基本用法
type Greeting = `hello ${string}`;
const g1: Greeting = "hello world"; // OK
const g2: Greeting = "hello typescript"; // OK4.2 聯合類型組合
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type Style = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green" | "medium-red" | ...4.3 方法名生成
type Fetcher = {
[K in keyof API as `fetch${Capitalize<string & K>}`]: () => Promise<API[K]>;
};
interface API {
user: User;
post: Post;
}
// 生成: fetchUser(): Promise<User> 和 fetchPost(): Promise<Post>4.4 路徑類型
type Path<T, P extends string = ""> =
T extends object
? {
[K in keyof T & string]: Path<T[K], `${P}.${K}`>;
}[keyof T & string]
: P;
interface Config {
db: { host: string; port: number };
redis: { url: string };
}
type ConfigPaths = Path<Config>;
// "db.host" | "db.port" | "redis.url"五、類型守衛
5.1 typeof 守衛
function process(value: string | number) {
if (typeof value === "string") {
value.toUpperCase(); // 類型收窄為 string
} else {
value.toFixed(2); // 類型收窄為 number
}
}5.2 instanceof 守衛
class NetworkError extends Error {
constructor(public statusCode: number) {
super();
}
}
function handleError(err: Error | NetworkError) {
if (err instanceof NetworkError) {
console.log(err.statusCode); // 類型收窄為 NetworkError
}
}5.3 in 守衛
interface Fish { swim: () => void }
interface Bird { fly: () => void }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // 類型收窄為 Fish
} else {
animal.fly(); // 類型收窄為 Bird
}
}5.4 自定義類型守衛
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(v => typeof v === "string");
}
function isError(value: unknown): value is Error {
return value instanceof Error;
}
// 使用
function handle(data: unknown) {
if (isStringArray(data)) {
data.forEach(s => s.toUpperCase()); // 類型為 string[]
}
}5.5 斷言函數
function assertDefined<T>(value: T | undefined | null): asserts value is T {
if (value === undefined || value === null) {
throw new Error(`Expected value to be defined, got ${value}`);
}
}
function processUser(user: User | undefined) {
assertDefined(user);
user.name; // 類型收窄為 User
}六、實用類型體操
6.1 DeepPartial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface Config {
db: { host: string; port: number };
cache: { ttl: number };
}
type PartialConfig = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; cache?: { ttl?: number } }6.2 DeepReadonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};6.3 Omit 與 Pick 組合
// 按類型選取屬性
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = PickByType<User, string>;
// { name: string; email: string }
type NumberProps = PickByType<User, number>;
// { age: number }6.4 聯合類型轉交叉類型
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }6.5 獲取對象路徑類型
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Get<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
interface Config {
db: { host: string; port: number };
}
type HostType = Get<Config, "db.host">; // string
type PortType = Get<Config, "db.port">; // number七、類型編程實戰
7.1 類型安全的 EventEmitter
type EventMap = Record<string, any>;
class TypedEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<Function>>();
on<K extends keyof Events>(event: K, fn: (payload: Events[K]) => void) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(fn);
return this;
}
emit<K extends keyof Events>(event: K, payload: Events[K]) {
this.listeners.get(event)?.forEach(fn => fn(payload));
}
off<K extends keyof Events>(event: K, fn: Function) {
this.listeners.get(event)?.delete(fn);
}
}
// 使用
interface MyEvents {
open: { url: string };
close: { code: number };
}
const emitter = new TypedEmitter<MyEvents>();
emitter.on("open", (e) => e.url); // e 類型: { url: string }
emitter.emit("close", { code: 100 }); // OK
// emitter.emit("close", { wrong: 1 }) // 編譯錯誤!7.2 類型安全的路由參數
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
// 使用
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
function route<T extends string>(
path: T,
params: ExtractParams<T>
): string {
let result = path;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value);
}
return result;
}
route("/users/:userId/posts/:postId", { userId: "1", postId: "2" }); // OK
// route("/users/:userId", {}) // 編譯錯誤!缺少 userId7.3 類型安全的狀態機
type Transition<S, M> = {
[K in keyof M]: K extends keyof S ? M[K] : never;
};
interface States {
idle: { type: "idle" };
loading: { type: "loading"; requestId: string };
success: { type: "success"; data: string };
error: { type: "error"; message: string };
}
type State = States[keyof States];
// { type: "idle" } | { type: "loading"; requestId: string } | ...
type Transitions = {
idle: "loading";
loading: "success" | "error";
success: "idle";
error: "idle";
};
function transition(
current: State,
next: Transitions[State["type"]]
): State {
// 狀態轉換邏輯
return { type: next } as State;
}八、類型性能優化
8.1 避免過度遞歸
// 慢:遞歸深度大
type DeepPartialSlow<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartialSlow<T[P]> : T[P];
};
// 快:限制遞歸深度
type DeepPartial<T, Depth extends number = 3> = {
[P in keyof T]?: Depth extends 0
? T[P]
: T[P] extends object
? DeepPartial<T[P], [...Tuple<Depth>, 0]["length"]>
: T[P];
};8.2 使用 satisfies 替代直接斷言
// TypeScript 4.9+
const config = {
port: 3000,
host: "localhost",
} satisfies Record<string, string | number>;
// config 推斷為 { port: number; host: string },而不是 Record<string, string | number>8.3 避免大聯合類型
// 不推薦:大聯合類型導致編譯慢
type Status = "idle" | "loading" | "success" | "error" | "timeout" | "offline" | "connecting" | "disconnected" | "paused" | "resuming";
// 推薦:使用枚舉或常量對象
const Status = {
IDLE: "idle",
LOADING: "loading",
SUCCESS: "success",
ERROR: "error",
} as const;
type Status = typeof Status[keyof typeof Status];九、常用工具類型速查
// 內置工具類型
Partial<T> // 所有屬性可選
Required<T> // 所有屬性必填
Readonly<T> // 所有屬性只讀
Pick<T, K> // 選取部分屬性
Omit<T, K> // 排除部分屬性
Record<K, V> // 鍵值對類型
ReturnType<T> // 函數返回類型
Parameters<T> // 函數參數類型
InstanceType<T> // 構造函數實例類型
NonNullable<T> // 排除 null 和 undefined
Awaited<T> // 解包 Promise
Uppercase<S> // 字符串大寫
Lowercase<S> // 字符串小寫
Capitalize<S> // 首字母大寫
Uncapitalize<S> // 首字母小寫
// 常用自定義工具類型
DeepPartial<T> // 深層可選
DeepReadonly<T> // 深層只讀
PickByType<T, U> // 按類型選取
Mutable<T> // 移除只讀
Optional<T, K> // 指定屬性可選十、總結
TypeScript 高級類型是構建大型應用的核心能力:
- 條件類型是類型層面的
if-else - infer 讓你從已有類型中提取信息
- 映射類型讓你變換和重塑類型
- 模板字面量類型讓字符串類型編程成為可能
- 類型守衛讓運行時類型檢查與編譯時類型安全無縫銜接
- 類型體操解決實際問題的利器,但要避免過度複雜
🔷 類型安全的代碼是最好的代碼。掌握 TypeScript 高級類型,讓你在編譯階段就消滅 Bug。
相關文章推薦: