跳至主要內容
Skip to content

自訂工具類型實戰

內建工具類型不夠用?本篇將教你建立強大的自訂工具類型,解決實際開發中的類型問題。


一、 深度類型操作

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'>;
// string

3.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) => number

4.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路徑生成模板字面量 + 遞迴
PromisifyPromise 包裝Parameters + ReturnType
Merge合併物件條件索引
Split分割字串infer + 模板字面量

> **進階技巧**:

  • 遞迴處理深度結構
  • as 進行鍵重映射
  • 模板字面量操作字串

進階挑戰

  1. 實作 DeepMutable<T>:深度移除 readonly
  2. 實作 RequiredKeys<T>:取得必填屬性的鍵
  3. 實作一個類型安全的 set 函式
typescript
// 練習 3 提示
function set<T, P extends PathOf<T>>(
  obj: T,
  path: P,
  value: PathValue<T, P>
): T;

延伸閱讀與資源