TypeScript 工具型別 (Utility Types)

TypeScript 提供了許多內建的工具型別 (Utility Types),用來進行常見的型別轉換。這些工具型別讓我們可以更方便地操作和轉換型別。

屬性修飾工具型別

Partial<T>

將所有屬性變成可選:

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// 等同於:
// {
//     id?: number;
//     name?: string;
//     email?: string;
// }

function updateUser(id: number, updates: Partial<User>): void {
  // 可以只更新部分屬性
}

updateUser(1, { name: '小明' });
updateUser(2, { email: 'test@example.com' });

Required<T>

將所有屬性變成必要:

interface Config {
  host?: string;
  port?: number;
  ssl?: boolean;
}

type RequiredConfig = Required<Config>;
// 等同於:
// {
//     host: string;
//     port: number;
//     ssl: boolean;
// }

const config: RequiredConfig = {
  host: 'localhost',
  port: 3000,
  ssl: true,
};

Readonly<T>

將所有屬性變成唯讀:

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;
// 等同於:
// {
//     readonly x: number;
//     readonly y: number;
// }

const origin: ReadonlyPoint = { x: 0, y: 0 };
// origin.x = 10;  // 錯誤:無法指派給 'x',因為它是唯讀屬性

選取與排除工具型別

Pick<T, K>

從 T 中選取指定的屬性:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type UserPreview = Pick<User, 'id' | 'name' | 'email'>;
// 等同於:
// {
//     id: number;
//     name: string;
//     email: string;
// }

function getUserPreview(user: User): UserPreview {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
  };
}

Omit<T, K>

從 T 中排除指定的屬性:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type UserWithoutPassword = Omit<User, 'password'>;
// 等同於:
// {
//     id: number;
//     name: string;
//     email: string;
// }

type PublicUser = Omit<User, 'password' | 'email'>;
// {
//     id: number;
//     name: string;
// }

Record 型別

Record<K, T>

建立一個物件型別,其鍵為 K,值為 T:

type Role = 'admin' | 'user' | 'guest';

interface Permission {
  read: boolean;
  write: boolean;
  delete: boolean;
}

type RolePermissions = Record<Role, Permission>;

const permissions: RolePermissions = {
  admin: { read: true, write: true, delete: true },
  user: { read: true, write: true, delete: false },
  guest: { read: true, write: false, delete: false },
};
// 動態鍵
type StringMap = Record<string, string>;

const translations: StringMap = {
  hello: '你好',
  goodbye: '再見',
  thanks: '謝謝',
};

聯合型別工具

Exclude<T, U>

從聯合型別 T 中排除 U:

type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type PrimaryColors = Exclude<AllColors, 'yellow'>;
// "red" | "green" | "blue"

type Numbers = 1 | 2 | 3 | 4 | 5;
type OddNumbers = Exclude<Numbers, 2 | 4>;
// 1 | 3 | 5

Extract<T, U>

從聯合型別 T 中提取 U:

type AllTypes = string | number | boolean | null | undefined;
type Primitives = Extract<AllTypes, string | number | boolean>;
// string | number | boolean

type Event = 'click' | 'focus' | 'blur' | 'change';
type FocusEvents = Extract<Event, 'focus' | 'blur'>;
// "focus" | "blur"

NonNullable<T>

從 T 中排除 null 和 undefined:

type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

type Value = number | null | undefined;
type NonNullValue = NonNullable<Value>;
// number

函式相關工具型別

ReturnType<T>

取得函式的回傳型別:

function createUser() {
  return { id: 1, name: '小明', email: 'ming@example.com' };
}

type User = ReturnType<typeof createUser>;
// { id: number; name: string; email: string }

// 搭配泛型函式
type NumberArrayReturn = ReturnType<() => number[]>;
// number[]

Parameters<T>

取得函式的參數型別(元組):

function greet(name: string, age: number): string {
  return `${name} is ${age} years old`;
}

type GreetParams = Parameters<typeof greet>;
// [name: string, age: number]

// 取得個別參數
type FirstParam = Parameters<typeof greet>[0]; // string
type SecondParam = Parameters<typeof greet>[1]; // number

ConstructorParameters<T>

取得類別建構子的參數型別:

class User {
  constructor(
    public name: string,
    public age: number
  ) {}
}

type UserConstructorParams = ConstructorParameters<typeof User>;
// [name: string, age: number]

InstanceType<T>

取得類別的實例型別:

class User {
  constructor(public name: string) {}
}

type UserInstance = InstanceType<typeof User>;
// User

function createInstance<T extends new (...args: any[]) => any>(
  Class: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new Class(...args);
}

字串操作型別

TypeScript 4.1+ 提供內建的字串操作型別:

Uppercase<S>

type Greeting = 'hello';
type LoudGreeting = Uppercase<Greeting>; // "HELLO"

Lowercase<S>

type Shout = 'HELLO';
type Whisper = Lowercase<Shout>; // "hello"

Capitalize<S>

type Word = 'hello';
type Title = Capitalize<Word>; // "Hello"

Uncapitalize<S>

type Title = 'Hello';
type Word = Uncapitalize<Title>; // "hello"

實用範例

建立表單型別

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// 新增使用者的表單(不需要 id 和 createdAt)
type CreateUserForm = Omit<User, 'id' | 'createdAt'>;

// 更新使用者的表單(所有欄位可選,但不包含 id)
type UpdateUserForm = Partial<Omit<User, 'id'>>;

// API 回應(全部唯讀)
type UserResponse = Readonly<User>;

API 資源型別

interface Resource {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

type CreateDTO<T extends Resource> = Omit<T, keyof Resource>;
type UpdateDTO<T extends Resource> = Partial<CreateDTO<T>>;

interface Post extends Resource {
  title: string;
  content: string;
  authorId: number;
}

type CreatePost = CreateDTO<Post>;
// { title: string; content: string; authorId: number }

type UpdatePost = UpdateDTO<Post>;
// { title?: string; content?: string; authorId?: number }

狀態管理

interface AppState {
  user: User | null;
  posts: Post[];
  isLoading: boolean;
  error: string | null;
}

// Reducer action 型別
type SetAction<K extends keyof AppState> = {
  type: `SET_${Uppercase<K & string>}`;
  payload: AppState[K];
};

// 產生所有可能的 action
type AppActions = {
  [K in keyof AppState]: SetAction<K>;
}[keyof AppState];

事件處理器型別

type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  focus: FocusEvent;
};

type EventHandler<K extends keyof EventMap> = (event: EventMap[K]) => void;

function on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
  // ...
}

on('click', (e) => {
  console.log(e.clientX); // e 是 MouseEvent
});

on('keydown', (e) => {
  console.log(e.key); // e 是 KeyboardEvent
});

組合工具型別

// 深度 Readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

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

// 使用
interface NestedConfig {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}

type PartialConfig = DeepPartial<NestedConfig>;
// 所有巢狀屬性都變成可選