自訂工具類型實戰
內建工具類型不夠用?本篇將教你建立強大的自訂工具類型,解決實際開發中的類型問題。
一、 深度類型操作
1.1 DeepPartial
深度可選:
typescript
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// 使用範例
interface Config {
server: {
host: string;
port: number;
};
database: {
url: string;
pool: {
min: number;
max: number;
};
};
}
type PartialConfig = DeepPartial<Config>;
// 所有層級都是可選的1.2 DeepReadonly
深度唯讀:
typescript
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 使用範例
type ReadonlyConfig = DeepReadonly<Config>;
// 所有層級都是唯讀的1.3 DeepRequired
深度必填:
typescript
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;二、 選取與過濾
2.1 PickByType
選取特定類型的屬性:
typescript
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// 使用範例
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
type StringProps = PickByType<User, string>;
// { name: string, email: string }
type BooleanProps = PickByType<User, boolean>;
// { isAdmin: boolean }2.2 OmitByType
排除特定類型的屬性:
typescript
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
// 使用範例
type NonStringProps = OmitByType<User, string>;
// { id: number, isAdmin: boolean }2.3 PickOptional
選取可選屬性:
typescript
type PickOptional<T> = {
[K in keyof T as {} extends Pick<T, K> ? K : never]: T[K];
};
// 使用範例
interface Props {
required: string;
optional?: number;
}
type OptionalProps = PickOptional<Props>;
// { optional?: number }2.4 PickRequired
選取必填屬性:
typescript
type PickRequired<T> = {
[K in keyof T as {} extends Pick<T, K> ? never : K]: T[K];
};
type RequiredProps = PickRequired<Props>;
// { required: string }三、 路徑類型
3.1 基本路徑
typescript
type PathOf<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? K | `${K}.${PathOf<T[K]>}`
: K
: never;
}[keyof T]
: never;
// 使用範例
interface User {
name: string;
address: {
city: string;
country: string;
};
}
type UserPaths = PathOf<User>;
// 'name' | 'address' | 'address.city' | 'address.country'3.2 根據路徑取得類型
typescript
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
// 使用範例
type CityType = PathValue<User, 'address.city'>;
// string3.3 安全取值函式
typescript
function get<T, P extends PathOf<T>>(obj: T, path: P): PathValue<T, P> {
const keys = (path as string).split('.');
let result: any = obj;
for (const key of keys) {
result = result[key];
}
return result;
}
// 使用
const user: User = {
name: 'John',
address: { city: 'Taipei', country: 'Taiwan' },
};
const city = get(user, 'address.city'); // city: string四、 函式類型操作
4.1 Promisify
將回傳值包裝成 Promise:
typescript
type Promisify<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => Promise<ReturnType<T>>;
// 使用範例
function add(a: number, b: number): number {
return a + b;
}
type AsyncAdd = Promisify<typeof add>;
// (a: number, b: number) => Promise<number>4.2 Curried
柯里化類型:
typescript
type Curried<F> = F extends (a: infer A, ...rest: infer R) => infer Ret
? R extends []
? (a: A) => Ret
: (a: A) => Curried<(...args: R) => Ret>
: never;
// 使用範例
function add(a: number, b: number, c: number): number {
return a + b + c;
}
type CurriedAdd = Curried<typeof add>;
// (a: number) => (b: number) => (c: number) => number4.3 PartialParameters
部分參數:
typescript
type PartialParameters<T extends (...args: any[]) => any> = Partial<
Parameters<T>
> extends infer P
? P extends any[]
? (...args: P) => ReturnType<T>
: never
: never;五、 物件操作
5.1 Merge
合併兩個物件類型:
typescript
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never;
};
// 使用範例
interface A {
a: string;
b: number;
}
interface B {
b: string;
c: boolean;
}
type AB = Merge<A, B>;
// { a: string, b: string, c: boolean }5.2 Diff
取得兩個物件的差異:
typescript
type Diff<T, U> = {
[K in
| Exclude<keyof T, keyof U>
| Exclude<keyof U, keyof T>]: K extends keyof T
? T[K]
: K extends keyof U
? U[K]
: never;
};
// 使用範例
type DiffAB = Diff<A, B>;
// { a: string, c: boolean }5.3 Intersection
取得交集:
typescript
type Intersection<T, U> = Pick<T, Extract<keyof T, keyof U>>;
// 使用範例
type CommonProps = Intersection<A, B>;
// { b: number }六、 陣列與元組
6.1 Flatten
扁平化陣列類型:
typescript
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
? First extends any[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: [];
// 使用範例
type Flat = Flatten<[1, [2, [3, 4]], 5]>;
// [1, 2, 3, 4, 5]6.2 TupleToUnion
元組轉聯合:
typescript
type TupleToUnion<T extends any[]> = T[number];
// 使用範例
type Union = TupleToUnion<['a', 'b', 'c']>;
// 'a' | 'b' | 'c'6.3 UnionToTuple
聯合轉元組(進階):
typescript
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type LastOfUnion<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer Last
? Last
: never;
type UnionToTuple<T, Last = LastOfUnion<T>> = [T] extends [never]
? []
: [...UnionToTuple<Exclude<T, Last>>, Last];
// 使用範例
type Tuple = UnionToTuple<'a' | 'b' | 'c'>;
// ['a', 'b', 'c'](順序可能不同)七、 字串操作
7.1 Split
分割字串:
typescript
type Split<
S extends string,
D extends string
> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
// 使用範例
type Parts = Split<'a-b-c', '-'>;
// ['a', 'b', 'c']7.2 Join
合併字串:
typescript
type Join<T extends string[], D extends string> = T extends []
? ''
: T extends [infer F extends string]
? F
: T extends [infer F extends string, ...infer R extends string[]]
? `${F}${D}${Join<R, D>}`
: never;
// 使用範例
type Joined = Join<['a', 'b', 'c'], '-'>;
// 'a-b-c'7.3 CamelCase
轉換為駝峰式:
typescript
type CamelCase<S extends string> = S extends `${infer T}-${infer U}`
? `${Lowercase<T>}${Capitalize<CamelCase<U>>}`
: Lowercase<S>;
// 使用範例
type Camel = CamelCase<'my-component-name'>;
// 'myComponentName'八、 實用範例
8.1 API 類型
typescript
// 建立 CRUD 類型
type CrudEndpoints<T extends string, Entity> = {
[K in `get${Capitalize<T>}`]: () => Promise<Entity>;
} & {
[K in `get${Capitalize<T>}s`]: () => Promise<Entity[]>;
} & {
[K in `create${Capitalize<T>}`]: (
data: Omit<Entity, 'id'>
) => Promise<Entity>;
} & {
[K in `update${Capitalize<T>}`]: (
id: number,
data: Partial<Entity>
) => Promise<Entity>;
} & {
[K in `delete${Capitalize<T>}`]: (id: number) => Promise<void>;
};
// 使用
interface User {
id: number;
name: string;
}
type UserApi = CrudEndpoints<'user', User>;
// {
// getUser: () => Promise<User>
// getUsers: () => Promise<User[]>
// createUser: (data: Omit<User, 'id'>) => Promise<User>
// updateUser: (id: number, data: Partial<User>) => Promise<User>
// deleteUser: (id: number) => Promise<void>
// }總結
| 類型 | 用途 | 核心技巧 |
|---|---|---|
DeepPartial | 深度可選 | 遞迴 |
PickByType | 按類型選取 | 條件映射 |
PathOf | 路徑生成 | 模板字面量 + 遞迴 |
Promisify | Promise 包裝 | Parameters + ReturnType |
Merge | 合併物件 | 條件索引 |
Split | 分割字串 | infer + 模板字面量 |
> **進階技巧**:
- 遞迴處理深度結構
as進行鍵重映射- 模板字面量操作字串
進階挑戰
- 實作
DeepMutable<T>:深度移除 readonly - 實作
RequiredKeys<T>:取得必填屬性的鍵 - 實作一個類型安全的
set函式
typescript
// 練習 3 提示
function set<T, P extends PathOf<T>>(
obj: T,
path: P,
value: PathValue<T, P>
): T;