TypeScript — 泛型與 Interface
泛型讓程式碼在保有型別安全的前提下具備彈性;Interface 定義物件契約。 兩者是 TypeScript 中最核心的抽象工具。
Interface
基本宣告與選用屬性
interface 定義物件的「形狀」(shape)。 屬性加 ? 為選用,加 readonly 為不可變。
interface User {
readonly id: number // 初始化後不可修改
name: string
email: string
age?: number // 選用屬性,型別為 number | undefined
role: 'admin' | 'user' // 字面值 union
}
const alice: User = { id: 1, name: 'Alice', email: 'a@a.com', role: 'admin' }
// alice.id = 2 ❌ 無法指派至唯讀屬性定義方法
方法可用兩種語法定義。方法簡寫(method shorthand)與屬性函數語法在strictFunctionTypes 下對 covariance / contravariance 的處理略有不同, 一般情境選擇其一保持一致即可。
interface Calculator {
// 方法簡寫(method shorthand)
add(a: number, b: number): number
// 屬性函數語法(property function)
subtract: (a: number, b: number) => number
// 多載宣告(overload signatures)
parse(value: string): number
parse(value: number): string
}extends — 介面繼承
Interface 可繼承多個介面,繼承後必須滿足所有父介面的屬性。 若子介面重新宣告同名屬性,型別必須是父介面型別的子型別。
interface Animal {
name: string
breathe(): void
}
interface Pet {
owner: string
}
// 多重繼承:同時繼承 Animal 與 Pet
interface Dog extends Animal, Pet {
breed: string
bark(): void
}
const rex: Dog = {
name: 'Rex',
owner: 'Alice',
breed: 'Labrador',
breathe() { console.log('...') },
bark() { console.log('Woof!') },
}Declaration Merging — 宣告合併
同名的 interface 會自動合併,這是 interface 獨有的特性(type alias 不支援)。 常用於擴充第三方套件的型別定義。
// 擴充 Express 的 Request 型別(實務中常見)
declare global {
namespace Express {
interface Request {
currentUser?: { id: string; role: string }
}
}
}
// ---
// 同一個檔案內也能合併
interface Config {
host: string
}
interface Config {
port: number
}
// 等同於 { host: string; port: number }
const cfg: Config = { host: 'localhost', port: 3000 }interface vs type alias — 如何選擇?
| 特性 | interface | type alias |
|---|---|---|
| 描述物件形狀 | ✓ | ✓ |
| Declaration Merging | ✓ | ✗ |
| 描述 Union / Intersection | ✗ | ✓ |
| Tuple / Primitive alias | ✗ | ✓ |
| Mapped Types / Conditional Types | ✗ | ✓ |
| implements(class 實作) | ✓ | ✓(物件型別) |
建議原則:定義物件 / API 回傳型別優先用 interface(可擴充、錯誤訊息更友善); 需要 Union、Tuple、Conditional Type 時改用 type。
泛型(Generics)
泛型的本質:型別參數
泛型是「接受型別作為參數」的機制。用 <T> 宣告型別參數, 讓函數 / 介面 / class 在不同型別下都能正確運作,同時保留型別資訊(不像 any 會遺失型別)。
// ❌ any:失去型別資訊,呼叫端拿到 any
function identity_any(value: any): any { return value }
// ❌ 針對每種型別重複寫:不可擴展
function identity_string(value: string): string { return value }
function identity_number(value: number): number { return value }
// ✅ 泛型:一個函數,保有型別
function identity<T>(value: T): T { return value }
const s = identity('hello') // T 推斷為 string,s: string
const n = identity(42) // T 推斷為 number,n: number
const b = identity<boolean>(true) // 明確指定 T泛型函數實例
多個型別參數、回傳型別推斷,以及 React 中常見的泛型 API 封裝。
// 多型別參數
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second]
}
const p = pair('age', 30) // [string, number]
// ---
// 陣列工具:回傳型別自動跟隨元素型別
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const num = first([1, 2, 3]) // number | undefined
const str = first(['a', 'b']) // string | undefined
// ---
// 封裝 fetch,帶型別安全
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json() as T
}
// 使用:明確告知預期回傳型別
const user = await fetchData<User>('/api/user/1')
// user.name ← TypeScript 知道 user 有 name 屬性泛型 Interface
Interface 加上型別參數,可描述通用資料結構。
// 通用 API 回應包裝
interface ApiResponse<T> {
data: T
status: number
message: string
timestamp: string
}
// 使用時帶入具體型別
type UserResponse = ApiResponse<User>
type PostListResponse = ApiResponse<Post[]>
// ---
// 通用分頁結構
interface PaginatedResult<T> {
items: T[]
total: number
page: number
pageSize: number
hasNextPage: boolean
}
// 實際型別:分頁使用者列表
type UserPage = PaginatedResult<User>
// ---
// 泛型 Repository 介面(後端 / 測試常見)
interface Repository<T, ID> {
findById(id: ID): Promise<T | null>
findAll(): Promise<T[]>
create(entity: Omit<T, 'id'>): Promise<T>
update(id: ID, partial: Partial<T>): Promise<T>
delete(id: ID): Promise<void>
}泛型約束(Generic Constraints)
用 extends 限制型別參數必須滿足某個形狀, 才能在函數內安全存取特定屬性。
// ❌ 沒有約束:無法存取 .length
function logLength<T>(value: T) {
console.log(value.length) // 錯誤:T 上不存在 length
}
// ✅ 約束 T 必須有 length 屬性
function logLength<T extends { length: number }>(value: T): T {
console.log(value.length)
return value
}
logLength('hello') // ✓ string 有 length
logLength([1, 2, 3]) // ✓ array 有 length
logLength(42) // ❌ number 沒有 length
// ---
// keyof 約束:確保 key 存在於物件
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { id: 1, name: 'Alice', role: 'admin' }
const name = getProperty(user, 'name') // string
const id = getProperty(user, 'id') // number
// getProperty(user, 'xyz') // ❌ 'xyz' 不在 user 的 key 中
// ---
// 約束繼承 interface
interface HasId {
id: number
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id)
}
// T 可以是任何有 id 屬性的型別
const user2 = findById(users, 1) // T 推斷為 User
const post = findById(posts, 5) // T 推斷為 Post泛型預設型別
類似函數預設參數,泛型也可以設預設型別,在不指定型別時使用。
// 預設型別為 string
interface Stack<T = string> {
push(item: T): void
pop(): T | undefined
peek(): T | undefined
size: number
}
const strStack: Stack = { ... } // Stack<string>(使用預設)
const numStack: Stack<number> = { ... } // Stack<number>
// ---
// React 元件常見的泛型 Props 模式
interface TableProps<T = Record<string, unknown>> {
data: T[]
columns: Array<{
key: keyof T
label: string
render?: (value: T[keyof T], row: T) => React.ReactNode
}>
}
function Table<T = Record<string, unknown>>({ data, columns }: TableProps<T>) {
// ...
}內建工具型別(Utility Types)
TypeScript 內建的泛型工具型別,底層都用 Mapped Type + Conditional Type 實作。
物件轉換類
interface User {
id: number
name: string
email: string
age: number
}
// Partial<T> — 所有屬性變為選用(PATCH 請求常用)
type UserPatch = Partial<User>
// { id?: number; name?: string; email?: string; age?: number }
// Required<T> — 所有屬性變為必填(和 Partial 相反)
type StrictUser = Required<User>
// Readonly<T> — 所有屬性變為唯讀
type ImmutableUser = Readonly<User>
const u: ImmutableUser = { id: 1, name: 'A', email: 'a@a', age: 20 }
// u.name = 'B' ❌
// Pick<T, K> — 只保留指定屬性
type UserCard = Pick<User, 'id' | 'name'>
// { id: number; name: string }
// Omit<T, K> — 排除指定屬性(POST 建立時排除 id)
type CreateUserDto = Omit<User, 'id'>
// { name: string; email: string; age: number }
// Record<K, V> — 以 K 為 key、V 為 value 的物件型別
type RoleMap = Record<'admin' | 'user' | 'guest', string[]>
// { admin: string[]; user: string[]; guest: string[] }型別操作類
type Status = 'pending' | 'active' | 'suspended' | 'deleted'
// Extract<T, U> — 取出 T 中屬於 U 的型別
type ActiveStatus = Extract<Status, 'pending' | 'active'>
// 'pending' | 'active'
// Exclude<T, U> — 從 T 中排除屬於 U 的型別
type VisibleStatus = Exclude<Status, 'deleted'>
// 'pending' | 'active' | 'suspended'
// NonNullable<T> — 排除 null 與 undefined
type SafeString = NonNullable<string | null | undefined>
// string
// ---
// ReturnType<T> — 取得函數回傳型別(不重複宣告)
function getUser() {
return { id: 1, name: 'Alice', createdAt: new Date() }
}
type UserFromFn = ReturnType<typeof getUser>
// { id: number; name: string; createdAt: Date }
// Parameters<T> — 取得函數參數型別 tuple
type FetchParams = Parameters<typeof fetch>
// [input: RequestInfo | URL, init?: RequestInit]
// Awaited<T> — 取得 Promise resolve 後的型別
type UserValue = Awaited<Promise<User>>
// User進階:條件型別與 infer
條件型別(Conditional Types)
語法類似三元運算子:T extends U ? X : Y。 讓型別可以根據條件動態決定。
// 基本條件型別 type IsString<T> = T extends string ? 'yes' : 'no' type A = IsString<string> // 'yes' type B = IsString<number> // 'no' // --- // 分配條件型別(Distributive):T 為 Union 時各自計算 type ToArray<T> = T extends unknown ? T[] : never type StringOrNum = ToArray<string | number> // string[] | number[] // (不是 (string | number)[],而是各自分配) // --- // infer — 在 extends 條件中「推斷」型別並給予名稱 // 自己實作 ReturnType type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never type R1 = MyReturnType<() => string> // string type R2 = MyReturnType<(n: number) => boolean> // boolean // 取得 Promise 內的型別 type UnwrapPromise<T> = T extends Promise<infer V> ? V : T type U1 = UnwrapPromise<Promise<User>> // User type U2 = UnwrapPromise<string> // string(不是 Promise,原樣回傳) // 取得陣列元素型別 type ElementType<T> = T extends (infer E)[] ? E : never type E1 = ElementType<User[]> // User type E2 = ElementType<string[]> // string
Mapped Types — 映射型別
遍歷型別的所有 key 並逐一轉換——這是工具型別底層的實作原理。
// 自己實作 Partial
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
// 自己實作 Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
// ---
// 將所有屬性值轉為 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 FilterByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
}
interface Mixed {
id: number
name: string
active: boolean
count: number
}
type OnlyNumbers = FilterByType<Mixed, number>
// { id: number; count: number }React 實戰:泛型元件
泛型 Select 元件
元件加泛型讓 value 和 onChange的型別保持一致,防止傳入錯誤型別。
interface SelectProps<T> {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
getValue: (option: T) => string | number
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={String(getValue(value))}
onChange={e => {
const selected = options.find(o => String(getValue(o)) === e.target.value)
if (selected !== undefined) onChange(selected)
}}
>
{options.map(option => (
<option key={String(getValue(option))} value={String(getValue(option))}>
{getLabel(option)}
</option>
))}
</select>
)
}
// 使用:T 自動推斷為 User
<Select
options={users}
value={selectedUser}
onChange={setSelectedUser}
getLabel={u => u.name}
getValue={u => u.id}
/>泛型 Custom Hook
封裝後端請求的通用 hook,型別安全地處理 loading / error / data。
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function useFetch<T>(url: string): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' })
useEffect(() => {
if (!url) return
setState({ status: 'loading' })
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json() as Promise<T>
})
.then(data => setState({ status: 'success', data }))
.catch(error => setState({ status: 'error', error }))
}, [url])
return state
}
// 使用:T 指定為 User[]
function UserList() {
const state = useFetch<User[]>('/api/users')
if (state.status === 'loading') return <p>載入中...</p>
if (state.status === 'error') return <p>{state.error.message}</p>
if (state.status === 'success') {
// TypeScript 確認:state.data 是 User[]
return <ul>{state.data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
return null
}常見面試考點
最關鍵的差異是 declaration merging(interface 可以,type 不行)與表達能力(type 可以表達 union、intersection、tuple 等複雜型別)。物件形狀優先用 interface,複雜型別運算用 type。
any 會完全放棄型別檢查,呼叫端拿到的也是 any。泛型會在呼叫時綁定具體型別,型別資訊完整保留。例如 identity<T>(v: T): T 傳入 string 就回傳 string,不是 any。
①約束型別參數:function f<T extends HasId>——限制 T 必須有 id 屬性。②條件型別:T extends string ? ... : ...——根據 T 是否符合形狀決定型別。
在條件型別的 extends 子句中,infer 可以「捕捉」並命名某個型別位置的推斷結果。最典型的用法是 ReturnType:從函數型別中提取回傳型別。
Partial<T> = { [K in keyof T]?: T[K] };Pick<T, K> = { [P in K]: T[P] };Omit<T, K> = Pick<T, Exclude<keyof T, K>>。三者都是 Mapped Type 的應用。