TypeScript 型別體操進階:從入門到精通
為什麼 2026 年必須掌握進階 TypeScript
2026 年,TypeScript 已經是前端和 Node.js 專案的標配。但大多數開發者只停留在「給變數加型別」的階段,真正的型別程式設計能力——泛型約束、條件型別、infer 推導、映射型別——才是區分初級和進階 TypeScript 開發者的分水嶺。掌握型別體操,你可以在編譯期捕獲更多錯誤,減少執行時 Bug,同時讓程式碼的自文件化能力大幅提升。
型別體操能解決什麼問題
- 編譯期校驗:將執行時錯誤提前到編譯期,如 API 回應欄位缺失、表單校驗規則不一致
- 程式碼提示增強:精準的智慧提示讓開發效率翻倍,減少查閱文件的頻率
- 重構安全網:型別系統是重構的最強保障,改一個欄位型別,所有參考處立即報錯
- 領域建模:用 branded types、discriminated unions 在型別層面表達業務規則
- 零執行時開銷:所有型別體操在編譯後全部擦除,不影響包體積和效能
從簡單型別到型別程式設計的演進
// 初級:給變數加型別
const username: string = '張三'
const age: number = 25
// 中級:使用泛型和介面
interface ApiResponse<T> {
code: number
data: T
message: string
}
// 進階:型別程式設計 - 在型別層面做計算
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
type User = {
name: string
profile: { avatar: string; bio: string }
}
type FrozenUser = DeepReadonly<User>
// { readonly name: string; readonly profile: { readonly avatar: string; readonly bio: string } }
💡 使用 JSON 轉 TypeScript 工具 快速從 API 回應產生型別定義,然後在此基礎上做進階型別程式設計。
泛型約束與預設值
extends 約束泛型範圍
泛型約束讓你限制型別參數的範圍,避免傳入不合法的型別。
// 約束 T 必須有 id 屬性
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id)
}
interface User { id: number; name: string }
interface Product { id: number; title: string; price: number }
const users: User[] = [{ id: 1, name: '張三' }]
const products: Product[] = [{ id: 1, title: 'TypeScript 書', price: 99 }]
findById(users, 1) // ✅ User | undefined
findById(products, 1) // ✅ Product | undefined
findById([1, 2, 3], 1) // ❌ number 沒有 id 屬性
keyof 約束屬性名
// 約束 K 必須是 T 的屬性名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '張三', age: 25, email: 'zhangsan@test.com' }
getProperty(user, 'name') // ✅ string
getProperty(user, 'age') // ✅ number
getProperty(user, 'phone') // ❌ 'phone' 不是 user 的屬性
泛型預設值
interface PaginatedResponse<T, Meta = { total: number; page: number }> {
data: T[]
meta: Meta
}
type UserList = PaginatedResponse<User>
// meta 預設為 { total: number; page: number }
type CustomMeta = { total: number; page: number; hasNext: boolean }
type UserListCustom = PaginatedResponse<User, CustomMeta>
// meta 使用自訂型別
多泛型約束組合
// T 必須同時滿足兩個約束
type RequireBoth<T extends A & B, A, B> = T
interface HasId { id: number }
interface HasName { name: string }
function mergeEntities<T extends HasId & HasName>(a: T, b: T): T[] {
return [a, b]
}
mergeEntities({ id: 1, name: '張三' }, { id: 2, name: '李四' }) // ✅
mergeEntities({ id: 1 }, { id: 2 }) // ❌ 缺少 name
條件型別:extends ? :
基本語法
條件型別就像型別層面的三元運算式:T extends U ? X : Y
type IsString<T> = T extends string ? 'yes' : 'no'
type A = IsString<string> // 'yes'
type B = IsString<number> // 'no'
type C = IsString<'hello'> // 'yes'(字面量型別也匹配 string)
分散式條件型別
當 T 是聯合型別時,條件型別會自動分發:
type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<string | number>
// 分發過程:ToArray<string> | ToArray<number>
// 結果:string[] | number[]
// 注意:這不是 (string | number)[]
阻止分發
用 [T] 包裹來阻止分發行為:
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never
type Result2 = ToArrayNoDistribute<string | number>
// 結果:(string | number)[],不會分發
實戰:型別過濾
// 從聯合型別中過濾掉某些型別
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never
type AllTypes = string | number | boolean | null | undefined
type NonNull = Exclude<AllTypes, null | undefined>
// string | number | boolean
type OnlyString = Extract<AllTypes, string>
// string
// 過濾物件型別中的某些屬性
type PickByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
}
interface FormState {
username: string
age: number
email: string
isActive: boolean
}
type StringFields = PickByValue<FormState, string>
// { username: string; email: string }
infer 關鍵字深度解析
infer 基礎:在條件型別中提取型別
infer 只能在 extends 條件型別中使用,用於宣告一個待推斷的型別變數。
// 提取函式回傳值型別
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() {
return { name: '張三', age: 25 }
}
type UserType = ReturnType<typeof getUser>
// { name: string; age: number }
// 提取函式參數型別
type Parameters<T> = T extends (...args: infer P) => any ? P : never
function greet(name: string, age: number): string {
return `Hello, ${name}, age ${age}`
}
type GreetParams = Parameters<typeof greet>
// [string, number]
提取 Promise 內部型別
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type P1 = Awaited<Promise<string>> // string
type P2 = Awaited<Promise<Promise<number>>> // number(遞迴解包)
type P3 = Awaited<string> // string(非 Promise 直接回傳)
提取陣列元素型別
type ElementOf<T> = T extends (infer E)[] ? E : never
type Items = ElementOf<string[]> // string
type Nums = ElementOf<number[]> // number
type Mixed = ElementOf<(string | number)[]> // string | number
// 提取元組第一個元素
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never
type First = Head<[string, number, boolean]> // string
// 提取元組最後一個元素
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type Tail = Last<[string, number, boolean]> // boolean
多 infer 協同推斷
// 提取類別建構函式的實例型別
type InstanceOf<T> = T extends new (...args: any[]) => infer I ? I : never
class UserService {
constructor(private id: number) {}
getUser() { return { id: this.id } }
}
type ServiceInstance = InstanceOf<typeof UserService>
// UserService
// 同時提取請求和回應型別
type ApiTypes<T> = T extends (req: infer Req) => Promise<infer Res>
? { request: Req; response: Res }
: never
async function updateUser(req: { id: number; name: string }): Promise<{ success: boolean }> {
return { success: true }
}
type UpdateUserTypes = ApiTypes<typeof updateUser>
// { request: { id: number; name: string }; response: { success: boolean } }
映射型別:從零實作內建工具型別
Record 實作
// 內建 Record<K, V> 的實作原理
type MyRecord<K extends keyof any, V> = {
[P in K]: V
}
type UserRoles = MyRecord<'admin' | 'editor' | 'viewer', boolean>
// { admin: boolean; editor: boolean; viewer: boolean }
// 實戰:建立列舉映射
type StatusMap = MyRecord<'pending' | 'active' | 'closed', { label: string; color: string }>
const statusConfig: StatusMap = {
pending: { label: '待處理', color: '#f59e0b' },
active: { label: '進行中', color: '#10b981' },
closed: { label: '已關閉', color: '#6b7280' },
}
Partial 和 Required 實作
// Partial:所有屬性變為可選
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
// Required:所有屬性變為必選
type MyRequired<T> = {
[K in keyof T]-?: T[K]
}
// 深層 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K]
}
interface Config {
db: { host: string; port: number }
cache: { ttl: number; max: number }
}
type PartialConfig = DeepPartial<Config>
// { db?: { host?: string; port?: number }; cache?: { ttl?: number; max?: number } }
Pick 和 Omit 實作
// Pick:選取部分屬性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// Omit:排除部分屬性
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
interface FullUser {
id: number
name: string
email: string
password: string
createdAt: string
}
// 安全的使用者資訊,排除密碼
type SafeUser = MyOmit<FullUser, 'password'>
// { id: number; name: string; email: string; createdAt: string }
// 只更新部分欄位
type UserUpdate = MyPartial<MyPick<FullUser, 'name' | 'email'>>
// { name?: string; email?: string }
鍵重映射(Key Remapping)
TypeScript 4.1+ 支援用 as 對鍵進行重映射:
// 屬性名加前綴
type AddPrefix<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K]
}
interface ApiData { name: string; age: number }
type PrefixedData = AddPrefix<ApiData, 'user'>
// { userName: string; userAge: number }
// 將所有屬性變為 getter
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }
模板字面量型別
基礎用法
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'
// CSS 屬性型別
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'
type CSSValue = `${number}${CSSUnit}`
const width: CSSValue = '100px' // ✅
const height: CSSValue = '50vh' // ✅
const invalid: CSSValue = 'abc' // ❌ 不匹配模板
結合映射型別實作型別安全的事件系統
type Events = {
click: { x: number; y: number }
keydown: { key: string; code: string }
resize: { width: number; height: number }
}
type EventHandlerMap = {
[K in keyof Events as `on${Capitalize<string & K>}`]: (payload: Events[K]) => void
}
type Listeners = EventHandlerMap
// {
// onClick: (payload: { x: number; y: number }) => void
// onKeyDown: (payload: { key: string; code: string }) => void
// onResize: (payload: { width: number; height: number }) => void
// }
const listeners: Listeners = {
onClick: (e) => console.log(e.x, e.y),
onKeyDown: (e) => console.log(e.key),
onResize: (e) => console.log(e.width),
}
字串解析型別
// 解析路由參數
type ParseRouteParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ParseRouteParams<Rest>
: S extends `${string}:${infer Param}`
? Param
: never
type Params = ParseRouteParams<'/user/:id/post/:postId'>
// 'id' | 'postId'
// 解析版本號
type ParseVersion<V extends string> =
V extends `${infer Major}.${infer Minor}.${infer Patch}`
? { major: Major; minor: Minor; patch: Patch }
: never
type V = ParseVersion<'1.2.3'>
// { major: '1'; minor: '2'; patch: '3' }
遞迴型別
深層 Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
const config: DeepReadonly<{
db: { host: string; port: number }
cache: { ttl: number }
}> = {
db: { host: 'localhost', port: 3306 },
cache: { ttl: 3600 },
}
// config.db.host = '127.0.0.1' // ❌ 唯讀
深層 Partial 與合併
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type DeepMerge<A, B> = {
[K in keyof A | keyof B]: K extends keyof B
? B[K] extends object
? K extends keyof A
? A[K] extends object
? DeepMerge<A[K], B[K]>
: B[K]
: B[K]
: B[K]
: K extends keyof A
? A[K]
: never
}
遞迴元組操作
// 元組反轉
type Reverse<T extends any[]> = T extends [infer H, ...infer R]
? [...Reverse<R>, H]
: []
type Reversed = Reverse<[1, 2, 3, 4]> // [4, 3, 2, 1]
// 元組扁平化
type Flatten<T extends any[]> = T extends [infer H, ...infer R]
? H extends any[]
? [...Flatten<H>, ...Flatten<R>]
: [H, ...Flatten<R>]
: []
type Flat = Flatten<[1, [2, 3], [4, [5, 6]]]> // [1, 2, 3, 4, 5, 6]
// 字串長度
type StrLen<S extends string, Acc extends any[] = []> =
S extends `${string}${infer Rest}`
? StrLen<Rest, [...Acc, 0]>
: Acc['length']
type Len = StrLen<'hello'> // 5
型別層級程式設計:算術與字串操作
型別層級加法
// 利用元組長度實作加法
type BuildTuple<N extends number, T extends any[] = []> =
T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>
type Add<A extends number, B extends number> =
[...BuildTuple<A>, ...BuildTuple<B>]['length']
type Sum = Add<3, 5> // 8
// 型別層級減法
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
? Rest['length']
: never
type Diff = Subtract<10, 4> // 6
型別層級字串操作
// 字串替換
type Replace<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${infer Before}${From}${infer After}`
? `${Before}${To}${After}`
: S
type Replaced = Replace<'hello world', 'world', 'TypeScript'>
// 'hello TypeScript'
// 駝峰轉底線
type CamelToSnake<S extends string> =
S extends `${infer H}${infer Rest}`
? H extends Uppercase<H>
? `_${Lowercase<H>}${CamelToSnake<Rest>}`
: `${H}${CamelToSnake<Rest>}`
: S
type Snake = CamelToSnake<'userName'> // 'user_name'
可辨識聯合型別
基礎模式
// 用 kind 欄位做辨識
interface Circle { kind: 'circle'; radius: number }
interface Rectangle { kind: 'rectangle'; width: number; height: number }
interface Triangle { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Rectangle | Triangle
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2
case 'rectangle': return shape.width * shape.height
case 'triangle': return 0.5 * shape.base * shape.height
}
}
狀態機模式
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function handleState<T>(state: RequestState<T>) {
switch (state.status) {
case 'idle': return '等待請求'
case 'loading': return '載入中...'
case 'success': return `資料: ${JSON.stringify(state.data)}`
case 'error': return `錯誤: ${state.error.message}`
}
}
// 狀態轉換是型別安全的
const idleState: RequestState<string> = { status: 'idle' }
// idleState.data // ❌ idle 狀態沒有 data
const successState: RequestState<string> = { status: 'success', data: 'hello' }
// successState.data // ✅ string
窮盡檢查
// 利用 never 型別確保所有分支都被處理
function assertNever(x: never): never {
throw new Error(`未處理的型別: ${JSON.stringify(x)}`)
}
function processShape(shape: Shape) {
switch (shape.kind) {
case 'circle': return /* ... */ ''
case 'rectangle': return /* ... */ ''
case 'triangle': return /* ... */ ''
default: return assertNever(shape) // 如果漏掉某個 case,這裡會報錯
}
}
Branded Types:型別安全的利器
基礎 Branded Type
// 用品牌型別區分相同結構但不同意義的型別
type Brand<T, B extends string> = T & { __brand: B }
type UserId = Brand<number, 'UserId'>
type OrderId = Brand<number, 'OrderId'>
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const uid = 1 as UserId
const oid = 2 as OrderId
getUser(uid) // ✅
getUser(oid) // ❌ OrderId 不能賦給 UserId
getOrder(1) // ❌ number 不能賦給 UserId
型別安全的 ID 系統
function createId<T extends string>(brand: T) {
return (id: number) => id as Brand<number, T>
}
const userId = createId('UserId')
const orderId = createId('OrderId')
function deleteUser(id: UserId) { /* ... */ }
deleteUser(userId(1)) // ✅
deleteUser(orderId(1)) // ❌ 型別安全阻止了錯誤
型別安全的貨幣系統
type USD = Brand<number, 'USD'>
type CNY = Brand<number, 'CNY'>
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD
}
function addCNY(a: CNY, b: CNY): CNY {
return (a + b) as CNY
}
const price1 = 100 as USD
const price2 = 50 as USD
const cnyPrice = 200 as CNY
addUSD(price1, price2) // ✅
addUSD(price1, cnyPrice) // ❌ 不能把 CNY 和 USD 混加
satisfies 運算子
基礎用法
satisfies 讓你在不拓寬型別的情況下驗證型別相容性:
// ❌ 用型別註解會拓寬型別
const colors: Record<string, string | string[]> = {
primary: '#3b82f6',
secondary: '#6b7280',
gradients: ['#3b82f6', '#6b7280'],
}
// colors.primary.toUpperCase() // ❌ 型別是 string | string[],不能直接 toUpperCase
// ✅ 用 satisfies 保持字面量型別
const themeColors = {
primary: '#3b82f6',
secondary: '#6b7280',
gradients: ['#3b82f6', '#6b7280'],
} satisfies Record<string, string | string[]>
themeColors.primary.toUpperCase() // ✅ 型別是 '#3b82f6'
themeColors.gradients[0].toUpperCase() // ✅ 型別是 string
配置物件校驗
interface FeatureConfig {
enabled: boolean
label: string
}
const features = {
darkMode: { enabled: true, label: '深色模式' },
notifications: { enabled: false, label: '通知' },
analytics: { enabled: true, label: '分析' },
} satisfies Record<string, FeatureConfig>
// features 的型別保留了所有鍵名,不會變成 Record<string, FeatureConfig>
features.darkMode.enabled // ✅ boolean
features.notifications.label // ✅ string
// features.nonExist.enabled // ❌ 屬性不存在
const 型別參數
泛型推斷字面量型別
TypeScript 5.0+ 的 const 型別參數讓泛型推斷保留字面量型別:
// ❌ 沒有 const,T 被推斷為 string
function createRoute<T>(path: T) {
return { path }
}
const route = createRoute('/user/:id')
// route.path 型別是 string,不是 '/user/:id'
// ✅ 使用 const 型別參數
function createRouteConst<const T>(path: T) {
return { path }
}
const routeConst = createRouteConst('/user/:id')
// routeConst.path 型別是 '/user/:id'
結合模板字面量型別
function defineEndpoints<const T extends Record<string, string>>(routes: T) {
type RouteKeys = keyof T
return routes as T & { _routeKeys: RouteKeys }
}
const api = defineEndpoints({
getUsers: '/api/users',
getUser: '/api/users/:id',
createUser: '/api/users',
})
// api 的鍵名被保留為字面量型別 'getUsers' | 'getUser' | 'createUser'
實戰模式
API 回應型別安全
interface ApiRoutes {
'/api/users': {
response: { users: { id: number; name: string }[] }
}
'/api/users/:id': {
params: { id: number }
response: { id: number; name: string; email: string }
}
'/api/orders': {
query: { status?: string; page?: number }
response: { orders: { id: number; total: number }[]; total: number }
}
}
type ApiRoute = keyof ApiRoutes
async function apiCall<
Route extends ApiRoute,
Config extends ApiRoutes[Route]
>(
route: Route,
options: Omit<Config, 'response'> extends object ? Config : {}
): Promise<Config extends { response: infer R } ? R : never> {
const res = await fetch(route as string)
return res.json()
}
// 型別安全的 API 呼叫
const users = await apiCall('/api/users', {})
// users 型別: { users: { id: number; name: string }[] }
表單校驗型別
type Validator<T> = (value: T) => string | null
type FormValidators<T> = {
[K in keyof T]?: Validator<T[K]> | Validator<T[K]>[]
}
interface LoginForm {
username: string
password: string
remember: boolean
}
const loginValidators: FormValidators<LoginForm> = {
username: (v) => v.length < 3 ? '使用者名稱至少3個字元' : null,
password: (v) => v.length < 6 ? '密碼至少6個字元' : null,
}
type ValidationError<T> = {
[K in keyof T]?: string[]
}
function validateForm<T>(
data: T,
validators: FormValidators<T>
): ValidationError<T> {
const errors: ValidationError<T> = {}
for (const key in validators) {
const validator = validators[key]
const value = data[key]
if (validator && value !== undefined) {
const result = Array.isArray(validator)
? validator.map(v => v(value)).filter(Boolean)
: validator(value)
if (result) errors[key] = Array.isArray(result) ? result : [result]
}
}
return errors
}
事件系統型別
interface EventMap {
'user:login': { userId: number; timestamp: number }
'user:logout': { userId: number }
'cart:add': { productId: number; quantity: number }
'cart:remove': { productId: number }
'order:create': { orderId: number; total: number }
}
type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void
class TypeSafeEventEmitter {
private handlers = new Map<string, Set<EventHandler<any>>>()
on<K extends keyof EventMap>(event: K, handler: EventHandler<K>) {
if (!this.handlers.has(event as string)) {
this.handlers.set(event as string, new Set())
}
this.handlers.get(event as string)!.add(handler)
return () => this.off(event, handler)
}
off<K extends keyof EventMap>(event: K, handler: EventHandler<K>) {
this.handlers.get(event as string)?.delete(handler)
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
this.handlers.get(event as string)?.forEach(h => h(payload))
}
}
const emitter = new TypeSafeEventEmitter()
emitter.on('user:login', (e) => console.log(e.userId)) // ✅
emitter.emit('cart:add', { productId: 1, quantity: 2 }) // ✅
// emitter.emit('cart:add', { wrong: true }) // ❌ 型別錯誤
常見型別錯誤及修復
錯誤 1:型別被意外拓寬
// ❌ 物件字面量型別被拓寬
const config = { port: 3000, host: 'localhost' }
// config.port 型別是 number,不是 3000
// ✅ 使用 as const
const configConst = { port: 3000, host: 'localhost' } as const
// configConst.port 型別是 3000
錯誤 2:泛型推斷為聯合型別
// ❌ T 被推斷為 string | number,不是聯合分發
function wrapInArray<T>(value: T): T[] {
return [value]
}
const result = wrapInArray(Math.random() > 0.5 ? 'hello' : 42)
// result 型別是 (string | number)[],不是 string[] | number[]
// ✅ 使用函式多載或條件型別
function wrapInArrayFixed<T>(value: T): [T] {
return [value]
}
錯誤 3:循環參考
// ❌ 型別循環參考
interface TreeNode {
value: string
children: TreeNode[] // ✅ 介面支援循環參考
}
// ❌ type 別名不支援循環參考
// type Node = { value: string; children: Node[] } // 可能報錯
// ✅ 使用介面或加入泛型間接參考
type Node<T = Node> = { value: string; children: T[] }
錯誤 4:any 污染
// ❌ any 會污染所有接觸它的型別
function parseJSON(str: string): any {
return JSON.parse(str)
}
const data = parseJSON('{"name":"張三"}')
data.nonExist.method() // 不報錯,但執行時當掉
// ✅ 使用 unknown
function parseJSONSafe(str: string): unknown {
return JSON.parse(str)
}
const safeData = parseJSONSafe('{"name":"張三"}')
// safeData.name // ❌ unknown 上不存在 name 屬性
if (typeof safeData === 'object' && safeData !== null && 'name' in safeData) {
console.log((safeData as { name: string }).name) // ✅
}
型別除錯技巧
檢視中間型別
// 利用型別錯誤檢視型別
type Debug<T> = { [K in keyof T]: T[K] }
type Result = Debug<SomeComplexType>
// 滑鼠懸停 Result 即可看到展開後的型別
// 使用工具型別輔助除錯
type Prettify<T> = { [K in keyof T]: T[K] } & {}
type Nested = { a: { b: { c: string } }; d: number }
type PrettyNested = Prettify<Nested>
// 展開為 { a: { b: { c: string } }; d: number }
條件型別逐步除錯
// 逐步拆解複雜條件型別
type Step1<T> = T extends string ? true : false
type Step2<T> = T extends `${infer H}${infer R}` ? { head: H; rest: R } : never
// 逐層驗證
type S1 = Step1<'hello'> // true
type S2 = Step2<'hello'> // { head: 'h'; rest: 'ello' }
常見問題 FAQ
Q1: 型別體操會影響執行時效能嗎?
不會。TypeScript 的所有型別資訊在編譯後都會被完全擦除,型別體操只存在於編譯期,對執行時效能零影響。
Q2: 什麼時候該用型別體操,什麼時候該用執行時校驗?
兩者搭配使用:型別體操負責編譯期校驗(開發時捕獲錯誤),執行時校驗(如 Zod、Joi)負責外部資料(API 回應、使用者輸入)的校驗。型別體操不能替代執行時校驗。
Q3: 遞迴型別深度有限制嗎?
有。TypeScript 對遞迴型別有深度限制(約 1000 層),超出會報「Type instantiation is excessively deep」錯誤。實際使用中,10 層以內的遞迴通常足夠。
Q4: 如何在團隊中推廣型別體操?
- 從簡單的工具型別開始(Partial、Pick、Omit)
- 建立團隊的型別工具庫(
types/utils.ts) - 在 Code Review 中要求關鍵函式有完整型別
- 使用 JSON 格式化工具 驗證 JSON 資料結構,搭配型別定義確保一致性
Q5: satisfies 和型別註解有什麼區別?
型別註解(:)會拓寬型別為註解型別,satisfies 只做校驗不改變推斷型別。需要保持字面量型別精度時用 satisfies,需要明確約束型別時用型別註解。
💡 使用 Base64 編碼工具 處理型別定義中的內嵌資料,減少外部依賴。
本站提供瀏覽器本地工具,免註冊即可試用 →