TypeScript Brand Type:型別安全ID與型別品牌實戰
前端开发
TypeScript Brand Type:型別安全ID與型別品牌實戰
TypeScript是結構化型別系統——只要形狀一樣,型別就相容。這意味著UserId和OrderId都是string時可以互相賦值,編譯器不會報錯。這在業務程式碼中是災難:把使用者ID傳給訂單查詢介面,執行時才發現錯誤。
Brand Type(型別品牌)是TypeScript中實現名義型別(Nominal Typing)的經典模式。它透過在型別上「烙印」一個唯一標記,讓結構相同的型別在編譯期就不相容。本文從基礎到生產級,給你一套完整的Brand Type實戰方案。
核心概念速覽
| 概念 | 結構化型別 | Brand Type(名義型別) | 差異 |
|---|---|---|---|
| 型別相容 | 結構相同即相容 | 即使結構相同也不相容 | ⭐⭐⭐⭐⭐ |
| 編譯期檢查 | 弱(ID混用不報錯) | 強(ID混用立即報錯) | ⭐⭐⭐⭐⭐ |
| 執行時開銷 | 無 | 無(型別擦除) | ⭐ |
| 程式碼可讀性 | 弱(都是string) | 強(語義明確) | ⭐⭐⭐⭐ |
| 重構安全性 | 低 | 高(型別變更自動傳播) | ⭐⭐⭐⭐⭐ |
| 學習成本 | 低 | 中 | ⭐⭐⭐ |
五大痛點分析
痛點1:原始ID型別混用無法偵測
// 結構化型別下的災難
function getUser(id: string) { /* ... */ }
function getOrder(id: string) { /* ... */ }
const userId = 'user_123'
const orderId = 'order_456'
getUser(orderId) // 編譯通過!執行時才發現傳錯了
getOrder(userId) // 編譯通過!執行時才發現傳錯了
痛點2:單位型別無法在編譯期區分
// 金額單位混用
function processPayment(amount: number, currency: string) { /* ... */ }
processPayment(100, 'CNY') // 100是分還是元?
processPayment(100, 'USD') // 100是美分還是美元?
痛點3:狀態轉換無法在型別層面約束
// 狀態機沒有型別約束
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'completed'
function transition(current: OrderStatus, next: OrderStatus) { /* ... */ }
transition('completed', 'pending') // 編譯通過!但業務上不允許
痛點4:API回應型別缺乏領域語義
// API回應都是原始型別,缺乏業務語義
interface ApiResponse {
id: string // 什麼ID?
parentId: string // 父級什麼ID?
refId: string // 引用什麼ID?
}
痛點5:跨模組型別傳遞缺乏安全保障
// 跨模組傳遞時型別資訊遺失
function parseInput(raw: string): number { return Number(raw) }
function calculateArea(width: number, height: number): number { /* ... */ }
const userId = parseInput('123') // 回傳number,但語義是UserId
calculateArea(userId, 100) // 編譯通過!但語義完全錯誤
五大核心模式實操
模式1:Brand Type基礎定義
執行環境: TypeScript 5.5+, Node.js 20+
// brand.ts - Brand Type核心定義
/**
* Brand Type基礎工具型別
* 透過交叉型別在原始型別上新增唯一品牌標記
* 執行時完全擦除,零開銷
*/
// ===== 1. 基礎Brand定義 =====
/**
* 建立品牌型別的通用工具
* @template T - 底層原始型別
* @template Brand - 品牌標記(唯一識別符)
*/
type Brand<T, Brand extends string> = T & { readonly __brand: Brand }
// 具體品牌型別定義
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
// 數值型品牌
type Milliseconds = Brand<number, 'Milliseconds'>
type Seconds = Brand<number, 'Seconds'>
type Pixels = Brand<number, 'Pixels'>
// ===== 2. 品牌建構器 =====
/**
* 型別安全的品牌值建構器
* 確保只有透過建構器才能建立品牌值
*/
function createUserId(value: string): UserId {
return value as UserId
}
function createOrderId(value: string): OrderId {
return value as OrderId
}
function createMilliseconds(value: number): Milliseconds {
return value as Milliseconds
}
// ===== 3. 編譯期型別安全驗證 =====
const userId = createUserId('user_123')
const orderId = createOrderId('order_456')
// ✅ 正確:型別匹配
function getUser(id: UserId): void { console.log('User:', id) }
getUser(userId) // OK
// ❌ 編譯錯誤:型別不匹配
// getUser(orderId) // Error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'
// ❌ 編譯錯誤:原始型別不能直接賦值
// const uid: UserId = 'raw_string' // Error: Type 'string' is not assignable to type 'UserId'
// brand-advanced.ts - 進階Brand Type模式
/**
* 不可變Brand:防止品牌值被修改
*/
type ImmutableBrand<T, Brand extends string> = Readonly<T> & {
readonly __brand: Brand
readonly __immutable: true
}
/**
* 可驗證Brand:建構時執行執行時驗證
*/
type ValidatedBrand<T, Brand extends string> = T & {
readonly __brand: Brand
readonly __validated: true
}
/**
* 建立帶驗證的品牌建構器
*/
function createValidatedBrand<T, Brand extends string>(
brand: Brand,
validate: (value: T) => boolean,
errorMessage: string
): (value: T) => ValidatedBrand<T, Brand> {
return (value: T): ValidatedBrand<T, Brand> => {
if (!validate(value)) {
throw new TypeError(`Validation failed for ${brand}: ${errorMessage}`)
}
return value as ValidatedBrand<T, Brand>
}
}
// 使用範例:信箱品牌型別
type Email = ValidatedBrand<string, 'Email'>
const createEmail = createValidatedBrand(
'Email',
(v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
'Invalid email format'
)
const email = createEmail('zhang@toolsku.com') // OK
// createEmail('not-an-email') // TypeError: Validation failed for Email: Invalid email format
// 使用範例:正整數品牌型別
type PositiveInt = ValidatedBrand<number, 'PositiveInt'>
const createPositiveInt = createValidatedBrand(
'PositiveInt',
(v: number) => Number.isInteger(v) && v > 0,
'Value must be a positive integer'
)
const count = createPositiveInt(42) // OK
// createPositiveInt(-1) // TypeError
// createPositiveInt(3.14) // TypeError
模式2:型別安全ID
// branded-ids.ts - 生產級型別安全ID系統
/**
* ID品牌型別系統
* 解決:不同實體ID混用、ID格式驗證、ID序列化/反序列化
*/
// ===== 1. ID品牌定義 =====
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
type TenantId = Brand<string, 'TenantId'>
type SessionId = Brand<string, 'SessionId'>
// UUID格式的ID品牌
type UuidBrand<T extends string> = ValidatedBrand<string, T>
type UserUuid = UuidBrand<'UserUuid'>
type OrderUuid = UuidBrand<'OrderUuid'>
// ===== 2. ID建構器工廠 =====
interface IdConfig {
prefix: string
separator?: string
validate?: (value: string) => boolean
}
function createIdFactory<T extends string>(config: IdConfig) {
const separator = config.separator ?? '_'
const validate = config.validate ?? ((v: string) => v.startsWith(config.prefix + separator))
function create(raw: string): Brand<string, T> {
if (!validate(raw)) {
throw new TypeError(
`Invalid ID format for ${config.prefix}: expected "${config.prefix}${separator}...", got "${raw}"`
)
}
return raw as Brand<string, T>
}
function generate(): Brand<string, T> {
const id = `${config.prefix}${separator}${crypto.randomUUID().slice(0, 8)}`
return id as Brand<string, T>
}
function isType(value: string): value is Brand<string, T> {
return validate(value)
}
return { create, generate, isType }
}
// ===== 3. 具體ID工廠 =====
const UserIdFactory = createIdFactory<'UserId'>({
prefix: 'usr',
validate: (v) => /^usr_[a-zA-Z0-9]{8,}$/.test(v),
})
const OrderIdFactory = createIdFactory<'OrderId'>({
prefix: 'ord',
validate: (v) => /^ord_[a-zA-Z0-9]{8,}$/.test(v),
})
const ProductIdFactory = createIdFactory<'ProductId'>({
prefix: 'prd',
validate: (v) => /^prd_[a-zA-Z0-9]{8,}$/.test(v),
})
// ===== 4. 使用範例 =====
// 產生新ID
const newUserId = UserIdFactory.generate() // "usr_a1b2c3d4"
const newOrderId = OrderIdFactory.generate() // "ord_e5f6g7h8"
// 從字串建立(帶驗證)
const existingUserId = UserIdFactory.create('usr_existing1')
// UserIdFactory.create('ord_wrong_prefix') // TypeError
// 型別守衛
function processId(raw: string) {
if (UserIdFactory.isType(raw)) {
// raw 被收窄為 UserId
getUser(raw) // OK
}
}
// ===== 5. ID在API型別中的使用 =====
interface UserDTO {
id: UserId
name: string
email: Email
tenantId: TenantId
}
interface OrderDTO {
id: OrderId
userId: UserId // 外鍵:型別安全
productId: ProductId // 外鍵:型別安全
amount: CNY // 貨幣品牌(見模式3)
createdAt: ISODateString // 時間品牌
}
// 編譯期防止ID混用
function getUser(id: UserId): UserDTO { /* ... */ }
function getOrder(id: OrderId): OrderDTO { /* ... */ }
// getUser(newOrderId) // ❌ 編譯錯誤
// getOrder(newUserId) // ❌ 編譯錯誤
getUser(newUserId) // ✅ 正確
getOrder(newOrderId) // ✅ 正確
// branded-id-serialization.ts - ID序列化與反序列化
/**
* 品牌ID的序列化/反序列化
* 處理API邊界:JSON → 品牌型別 → JSON
*/
// 序列化:品牌型別 → 原始型別(自動,型別擦除)
function serializeUser(user: UserDTO): object {
return {
id: user.id as string, // Brand → string
name: user.name,
email: user.email as string, // Brand → string
tenantId: user.tenantId as string,
}
}
// 反序列化:原始型別 → 品牌型別(需要驗證)
function deserializeUser(raw: unknown): UserDTO {
if (typeof raw !== 'object' || raw === null) {
throw new TypeError('Invalid user data')
}
const data = raw as Record<string, unknown>
return {
id: UserIdFactory.create(data.id as string),
name: data.name as string,
email: createEmail(data.email as string),
tenantId: data.tenantId as TenantId,
}
}
// Zod整合:執行時驗證 + 編譯期型別安全
import { z } from 'zod'
const UserIdSchema = z.string().regex(/^usr_[a-zA-Z0-9]{8,}$/).transform((v) => UserIdFactory.create(v))
const OrderIdSchema = z.string().regex(/^ord_[a-zA-Z0-9]{8,}$/).transform((v) => OrderIdFactory.create(v))
const EmailSchema = z.string().email().transform((v) => createEmail(v))
const UserDTOSchema = z.object({
id: UserIdSchema,
name: z.string().min(1),
email: EmailSchema,
tenantId: z.string().transform((v) => v as TenantId),
})
// API回應解析
function parseUserResponse(data: unknown): UserDTO {
return UserDTOSchema.parse(data) // 執行時驗證 + 品牌型別
}
模式3:貨幣單位型別
// branded-currency.ts - 型別安全的貨幣系統
/**
* 貨幣品牌型別
* 解決:金額單位混用(分/元)、幣種混用、精度遺失
*/
// ===== 1. 貨幣品牌定義 =====
type CNY = Brand<number, 'CNY'> // 人民幣(分)
type USD = Brand<number, 'USD'> // 美元(美分)
type JPY = Brand<number, 'JPY'> // 日圓(元,無小數)
// 帶精度的金額品牌
type CNY_Yuan = Brand<number, 'CNY_Yuan'> // 人民幣(元)
// ===== 2. 貨幣建構器 =====
function createCNY(fen: number): CNY {
if (!Number.isInteger(fen)) {
throw new TypeError('CNY amount must be in fen (integer)')
}
if (fen < 0) {
throw new TypeError('CNY amount must be non-negative')
}
return fen as CNY
}
function createUSD(cents: number): USD {
if (!Number.isInteger(cents)) {
throw new TypeError('USD amount must be in cents (integer)')
}
if (cents < 0) {
throw new TypeError('USD amount must be non-negative')
}
return cents as USD
}
function createJPY(yen: number): JPY {
if (!Number.isInteger(yen)) {
throw new TypeError('JPY amount must be in yen (integer)')
}
if (yen < 0) {
throw new TypeError('JPY amount must be non-negative')
}
return yen as JPY
}
// ===== 3. 型別安全的貨幣運算 =====
function addCNY(a: CNY, b: CNY): CNY {
return createCNY((a as number) + (b as number))
}
function subtractCNY(a: CNY, b: CNY): CNY {
const result = (a as number) - (b as number)
if (result < 0) {
throw new RangeError('CNY subtraction result cannot be negative')
}
return createCNY(result)
}
function multiplyCNY(amount: CNY, factor: number): CNY {
return createCNY(Math.round((amount as number) * factor))
}
// ❌ 編譯錯誤:不同幣種不能直接運算
// function addCNY_USD(a: CNY, b: USD): CNY { ... } // 型別不匹配
// ===== 4. 貨幣轉換(需要匯率) =====
interface ExchangeRate {
from: string
to: string
rate: number
updatedAt: Date
}
function convertCNYtoUSD(cny: CNY, rate: ExchangeRate): USD {
if (rate.from !== 'CNY' || rate.to !== 'USD') {
throw new TypeError('Invalid exchange rate direction')
}
const usdCents = Math.round((cny as number) * rate.rate / 100)
return createUSD(usdCents)
}
// ===== 5. 格式化輸出 =====
function formatCNY(amount: CNY): string {
const yuan = (amount as number) / 100
return `¥${yuan.toFixed(2)}`
}
function formatUSD(amount: USD): string {
const dollars = (amount as number) / 100
return `$${dollars.toFixed(2)}`
}
function formatJPY(amount: JPY): string {
return `¥${amount as number}`
}
// ===== 6. 使用範例 =====
const price = createCNY(9900) // 99.00元
const discount = createCNY(1000) // 10.00元
const finalPrice = subtractCNY(price, discount) // 89.00元
console.log(formatCNY(finalPrice)) // ¥89.00
// const wrongPrice = createUSD(9900) // USD型別
// addCNY(price, wrongPrice) // ❌ 編譯錯誤:USD不能賦值給CNY參數
模式4:狀態機型別
// branded-state-machine.ts - 型別安全的狀態機
/**
* 狀態機品牌型別
* 解決:非法狀態轉換、狀態遺漏、轉換副作用無約束
*/
// ===== 1. 品牌狀態定義 =====
type PendingOrder = Brand<'pending', 'PendingOrder'>
type PaidOrder = Brand<'paid', 'PaidOrder'>
type ShippedOrder = Brand<'shipped', 'ShippedOrder'>
type CompletedOrder = Brand<'completed', 'CompletedOrder'>
type CancelledOrder = Brand<'cancelled', 'CancelledOrder'>
// ===== 2. 型別安全的狀態轉換 =====
// 只允許合法的狀態轉換
function payOrder(_order: PendingOrder): PaidOrder {
return 'paid' as PaidOrder
}
function shipOrder(_order: PaidOrder): ShippedOrder {
return 'shipped' as ShippedOrder
}
function completeOrder(_order: ShippedOrder): CompletedOrder {
return 'completed' as CompletedOrder
}
function cancelPendingOrder(_order: PendingOrder): CancelledOrder {
return 'cancelled' as CancelledOrder
}
function cancelPaidOrder(_order: PaidOrder): CancelledOrder {
return 'cancelled' as CancelledOrder
}
// ❌ 編譯錯誤:非法轉換
// function unshipOrder(_order: CompletedOrder): ShippedOrder { ... } // 不存在此函數
// function repayOrder(_order: ShippedOrder): PaidOrder { ... } // 不存在此函數
// ===== 3. 帶資料的品牌狀態 =====
interface OrderStateData<S extends string> {
status: Brand<S, `Order_${S}`>
updatedAt: Date
updatedBy: UserId
}
type PendingOrderState = OrderStateData<'pending'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
}
type PaidOrderState = OrderStateData<'paid'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
paidAt: Date
paymentMethod: 'alipay' | 'wechat' | 'card'
transactionId: string
}
type ShippedOrderState = OrderStateData<'shipped'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
paidAt: Date
shippedAt: Date
trackingNumber: string
carrier: string
}
// ===== 4. 狀態轉換函數(帶資料) =====
function payOrderWithData(
order: PendingOrderState,
paymentMethod: 'alipay' | 'wechat' | 'card',
transactionId: string
): PaidOrderState {
return {
status: 'paid' as Brand<'paid', 'Order_paid'>,
updatedAt: new Date(),
updatedBy: order.updatedBy,
items: order.items,
paidAt: new Date(),
paymentMethod,
transactionId,
}
}
function shipOrderWithData(
order: PaidOrderState,
trackingNumber: string,
carrier: string
): ShippedOrderState {
return {
status: 'shipped' as Brand<'shipped', 'Order_shipped'>,
updatedAt: new Date(),
updatedBy: order.updatedBy,
items: order.items,
paidAt: order.paidAt,
shippedAt: new Date(),
trackingNumber,
carrier,
}
}
// ===== 5. 窮舉檢查 =====
type AnyOrderState = PendingOrderState | PaidOrderState | ShippedOrderState
function handleOrderState(state: AnyOrderState): string {
switch (state.status as string) {
case 'pending':
return '等待支付'
case 'paid':
return '已支付,等待發貨'
case 'shipped':
return '已發貨,等待確認收貨'
// 如果遺漏了狀態,TypeScript會在編譯期報錯
default:
const _exhaustive: never = state.status
throw new Error(`Unhandled state: ${_exhaustive}`)
}
}
模式5:生產級Brand Type工具庫
// brand-toolkit.ts - 生產級Brand Type工具庫
/**
* 完整的Brand Type工具集
* 覆蓋:建立、驗證、序列化、組合、映射
*/
// ===== 1. 核心工具型別 =====
/** 基礎品牌型別 */
type Brand<T, B extends string> = T & { readonly __brand: B }
/** 提取品牌標記 */
type ExtractBrand<B> = B extends Brand<infer T, infer _>
? _ extends string ? _ : never
: never
/** 提取底層型別 */
type Unbrand<B> = B extends Brand<infer T, infer _> ? T : B
/** 判斷是否是品牌型別 */
type IsBranded<T> = T extends Brand<any, any> ? true : false
// ===== 2. 品牌建立工具 =====
interface BrandCreatorConfig<T, B extends string> {
name: B
validate?: (value: T) => boolean
normalize?: (value: T) => T
errorMessage?: string
}
function createBrandCreator<T, B extends string>(
config: BrandCreatorConfig<T, B>
) {
const { name, validate, normalize, errorMessage } = config
function create(value: T): Brand<T, B> {
const normalized = normalize ? normalize(value) : value
if (validate && !validate(normalized)) {
throw new TypeError(
errorMessage ?? `Invalid value for brand "${name}": ${String(normalized)}`
)
}
return normalized as Brand<T, B>
}
function isType(value: T): value is Brand<T, B> {
return validate ? validate(value) : true
}
function unsafeCast(value: T): Brand<T, B> {
return value as Brand<T, B>
}
function unwrap(branded: Brand<T, B>): T {
return branded as T
}
return { create, isType, unsafeCast, unwrap, name }
}
// ===== 3. 常用品牌建立器 =====
// 手機號品牌
const PhoneNumber = createBrandCreator<string, 'PhoneNumber'>({
name: 'PhoneNumber',
validate: (v) => /^1[3-9]\d{9}$/.test(v),
normalize: (v) => v.replace(/\s/g, ''),
errorMessage: 'Invalid Chinese phone number format',
})
// URL品牌
const SecureUrl = createBrandCreator<string, 'SecureUrl'>({
name: 'SecureUrl',
validate: (v) => /^https:\/\//.test(v),
errorMessage: 'URL must use HTTPS protocol',
})
// 非空字串品牌
const NonEmptyString = createBrandCreator<string, 'NonEmptyString'>({
name: 'NonEmptyString',
validate: (v) => v.trim().length > 0,
normalize: (v) => v.trim(),
errorMessage: 'String must not be empty',
})
// 正整數品牌
const PositiveInteger = createBrandCreator<number, 'PositiveInteger'>({
name: 'PositiveInteger',
validate: (v) => Number.isInteger(v) && v > 0,
errorMessage: 'Value must be a positive integer',
})
// ===== 4. 品牌組合 =====
type BrandedEmail = Brand<string, 'Email'> & Brand<string, 'NonEmpty'>
const BrandedEmailCreator = {
create(value: string): BrandedEmail {
const trimmed = value.trim()
if (trimmed.length === 0) {
throw new TypeError('Email must not be empty')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
throw new TypeError('Invalid email format')
}
return trimmed as BrandedEmail
},
unwrap(value: BrandedEmail): string {
return value as string
},
}
// ===== 5. 品牌映射工具 =====
function mapBranded<T, B extends string, R>(
branded: Brand<T, B>,
mapper: (value: T) => R
): R {
return mapper(branded as T)
}
function mapBrandedTo<T, B extends string, R, RB extends string>(
branded: Brand<T, B>,
mapper: (value: T) => R,
targetBrand: RB
): Brand<R, RB> {
return mapper(branded as T) as Brand<R, RB>
}
// 使用範例
const userId = UserIdFactory.create('usr_abc12345')
const userIdLength = mapBranded(userId, (id) => id.length) // number
// ===== 6. 品牌型別守衛集合 =====
function createTypeGuard<T, B extends string>(
validate: (value: T) => boolean
): (value: T) => value is Brand<T, B> {
return (value: T): value is Brand<T, B> => validate(value)
}
const isUserId = createTypeGuard<string, 'UserId'>(
(v) => /^usr_[a-zA-Z0-9]{8,}$/.test(v)
)
const isOrderId = createTypeGuard<string, 'OrderId'>(
(v) => /^ord_[a-zA-Z0-9]{8,}$/.test(v)
)
// 使用
function processUnknownId(raw: string) {
if (isUserId(raw)) {
getUser(raw) // raw: UserId
} else if (isOrderId(raw)) {
getOrder(raw) // raw: OrderId
}
}
// ===== 7. 品牌型別註冊表 =====
class BrandRegistry {
private creators = new Map<string, ReturnType<typeof createBrandCreator<any, any>>>()
register<T, B extends string>(name: B, creator: ReturnType<typeof createBrandCreator<T, B>>): void {
this.creators.set(name, creator)
}
get<B extends string>(name: B): ReturnType<typeof createBrandCreator<any, B>> | undefined {
return this.creators.get(name)
}
validate(name: string, value: unknown): boolean {
const creator = this.creators.get(name)
if (!creator) return false
try {
creator.create(value)
return true
} catch {
return false
}
}
}
// 全域註冊
const brandRegistry = new BrandRegistry()
brandRegistry.register('PhoneNumber', PhoneNumber)
brandRegistry.register('SecureUrl', SecureUrl)
brandRegistry.register('NonEmptyString', NonEmptyString)
brandRegistry.register('PositiveInteger', PositiveInteger)
五大避坑指南
坑1:品牌型別斷言濫用
// ❌ 錯誤:到處使用as斷言繞過型別檢查
const userId = 'raw_string' as UserId // 繞過了驗證
// ✅ 正確:只透過建構器建立品牌值
const userId = UserIdFactory.create('usr_abc12345')
坑2:品牌型別與泛型衝突
// ❌ 錯誤:泛型參數遺失品牌資訊
function firstElement<T>(arr: T[]): T { return arr[0] }
const ids: UserId[] = [UserIdFactory.create('usr_1')]
const result = firstElement(ids) // T = UserId,OK
// 但如果泛型推斷為string,品牌資訊遺失
function processValue<T extends string>(value: T): string { return value }
processValue(userId) // 回傳string,遺失UserId品牌
// ✅ 正確:保持品牌型別傳遞
function processBranded<T extends Brand<string, any>>(value: T): Unbrand<T> {
return value as Unbrand<T>
}
坑3:品牌型別在條件型別中的行為
// ❌ 錯誤:條件型別中品牌資訊可能遺失
type UnwrapIfBranded<T> = T extends Brand<infer U, any> ? U : T
type Result = UnwrapIfBranded<UserId> // string,品牌資訊遺失
// ✅ 正確:如果需要保持品牌,不要解包
type KeepBrand<T> = T // 保持原樣
坑4:品牌型別與JSON序列化
// ❌ 錯誤:JSON.parse後品牌資訊遺失
const user: UserDTO = { id: userId, name: '老張', email: createEmail('z@t.com'), tenantId: 't1' as TenantId }
const json = JSON.stringify(user)
const parsed = JSON.parse(json) as UserDTO // 不安全!沒有執行時驗證
// ✅ 正確:使用Zod或自定義解析器驗證
const parsed = UserDTOSchema.parse(JSON.parse(json)) // 執行時驗證 + 品牌型別
坑5:品牌型別的as const與字面量衝突
// ❌ 錯誤:as const使品牌標記變成字面量型別
const status = 'pending' as const as PendingOrder // 型別變成 'pending' & { __brand: 'PendingOrder' }
// ✅ 正確:品牌建構器內部處理斷言
const status = createPendingOrder() // 函數回傳型別明確
報錯排查表
| 報錯資訊 | 原因 | 解決方案 |
|---|---|---|
Type 'string' is not assignable to type 'UserId' |
原始字串不能直接賦值給品牌型別 | 使用品牌建構器建立值 |
Argument of type 'OrderId' is not assignable to parameter of type 'UserId' |
不同品牌型別不相容 | 檢查是否傳錯了ID型別 |
Type 'Brand<string, "UserId">' is not assignable to type 'string' |
品牌型別不能直接賦值給原始型別 | 使用unwrap或as string解包 |
Conversion of type 'string' to type 'UserId' may be a mistake |
直接as斷言可能不安全 | 使用品牌建構器而非直接斷言 |
Property '__brand' does not exist on type 'string' |
執行時存取品牌標記 | 品牌標記僅編譯期存在,執行時已擦除 |
Type instantiation is excessively deep |
品牌型別巢狀過深 | 簡化品牌組合層級,拆分為多個獨立品牌 |
Cannot find module 'zod' |
Zod未安裝 | npm install zod |
This expression is not callable |
品牌建構器未正確匯出 | 檢查模組匯出和匯入路徑 |
Type 'never' is not assignable |
窮舉檢查中遺漏了狀態 | 補充遺漏的狀態分支 |
Generic type 'Brand' requires 2 type argument(s) |
Brand缺少品牌標記參數 | 補充品牌標記字串,如Brand<string, 'MyBrand'> |
五大進階優化技巧
技巧1:模板字面量品牌
// 模板字面量型別 + Brand:自動產生實體ID品牌
type EntityId<Entity extends string> = Brand<string, `${Entity}Id`>
type UserId = EntityId<'User'> // Brand<string, 'UserId'>
type OrderId = EntityId<'Order'> // Brand<string, 'OrderId'>
type ProductId = EntityId<'Product'> // Brand<string, 'ProductId'>
// 通用ID工廠
function createEntityIdFactory<Entity extends string>(entity: Entity) {
const prefix = entity.toLowerCase().slice(0, 3)
return createIdFactory<`${Entity}Id`>({
prefix,
validate: (v) => new RegExp(`^${prefix}_[a-zA-Z0-9]{8,}$`).test(v),
})
}
技巧2:品牌型別與Effect-TS整合
// 使用Effect的Brand模組(更安全)
import { Brand } from 'effect'
// Effect的Brand提供更完善的執行時驗證
const UserId = Brand.struct({
__brand: Brand.literal('UserId'),
value: Brand.string.pipe(Brand.pattern(/^usr_[a-zA-Z0-9]{8,}$/)),
})
type UserId = Brand.Brand.Construct<typeof UserId>
技巧3:品牌型別與Prisma整合
// Prisma模型使用品牌型別
import { Prisma } from '@prisma/client'
// Prisma產生的型別是原始型別,透過工具型別新增品牌
type BrandedUser = {
id: UserId
name: string
email: Email
createdAt: ISODateString
}
// Prisma結果 → 品牌型別
function toBrandedUser(prismaUser: Prisma.UserGetPayload<{}>): BrandedUser {
return {
id: UserIdFactory.create(prismaUser.id),
name: prismaUser.name,
email: createEmail(prismaUser.email),
createdAt: prismaUser.createdAt as ISODateString,
}
}
技巧4:品牌型別與tRPC整合
// tRPC過程使用品牌型別
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: UserIdSchema }))
.query(async ({ input }): Promise<UserDTO> => {
// input.id 已經是 UserId 品牌型別
return getUserById(input.id)
}),
createOrder: t.procedure
.input(z.object({
userId: UserIdSchema,
productId: ProductIdSchema,
amount: z.number().int().positive().transform((v) => createCNY(v)),
}))
.mutation(async ({ input }): Promise<OrderDTO> => {
// 所有ID和金額都是品牌型別
return createOrder(input.userId, input.productId, input.amount)
}),
})
技巧5:品牌型別效能監控
// brand-perf.ts - 品牌型別執行時驗證效能監控
interface BrandValidationMetrics {
brandName: string
totalValidations: number
failedValidations: number
averageValidationTime: number // μs
}
class BrandPerfMonitor {
private metrics = new Map<string, {
total: number
failed: number
times: number[]
}>()
recordValidation(brandName: string, duration: number, success: boolean): void {
const metric = this.metrics.get(brandName) ?? { total: 0, failed: 0, times: [] }
metric.total++
if (!success) metric.failed++
metric.times.push(duration)
this.metrics.set(brandName, metric)
}
getMetrics(): BrandValidationMetrics[] {
return Array.from(this.metrics.entries()).map(([name, metric]) => ({
brandName: name,
totalValidations: metric.total,
failedValidations: metric.failed,
averageValidationTime:
metric.times.reduce((a, b) => a + b, 0) / metric.times.length,
}))
}
}
// 包裝品牌建構器以新增監控
function withPerfMonitor<T, B extends string>(
creator: ReturnType<typeof createBrandCreator<T, B>>,
monitor: BrandPerfMonitor
): typeof creator {
const originalCreate = creator.create.bind(creator)
return {
...creator,
create(value: T): Brand<T, B> {
const start = performance.now()
try {
const result = originalCreate(value)
const duration = (performance.now() - start) * 1000 // μs
monitor.recordValidation(creator.name, duration, true)
return result
} catch (error) {
const duration = (performance.now() - start) * 1000
monitor.recordValidation(creator.name, duration, false)
throw error
}
},
}
}
對比分析表
| 維度 | 原始型別 | Brand Type | Zod Brand | io-ts | Effect Brand |
|---|---|---|---|---|---|
| 編譯期安全 | ❌ | ✅ | ✅ | ✅ | ✅ |
| 執行時驗證 | ❌ | 需手動 | ✅ 內建 | ✅ 內建 | ✅ 內建 |
| 執行時開銷 | 無 | 無 | 有 | 有 | 有 |
| 學習成本 | 低 | 中 | 中 | 高 | 中 |
| 生態整合 | 完整 | 需適配 | tRPC友善 | FP生態 | Effect生態 |
| 序列化 | 自動 | 需處理 | 自動 | 自動 | 自動 |
| 型別推導 | 自動 | 需標注 | 自動 | 複雜 | 自動 |
| 包體積 | 0 | 0 | ~13KB | ~8KB | ~50KB |
總結
TypeScript Brand Type是2026年型別安全程式設計的必備模式。核心要點:
- 基礎定義:透過交叉型別
T & { __brand: B }實現名義型別,零執行時開銷 - 型別安全ID:ID工廠模式統一建立、驗證、型別守衛,杜絕ID混用
- 貨幣單位:品牌型別約束幣種和單位,編譯期防止不同貨幣直接運算
- 狀態機:品牌狀態限定合法轉換路徑,窮舉檢查確保無遺漏
- 生產級工具庫:品牌建立器、註冊表、Zod整合、效能監控,覆蓋完整生命週期
Brand Type讓TypeScript從「結構相容就行」進化到「語義不對就不行」。在ID、金額、狀態這些關鍵領域,品牌型別是防止執行時bug的第一道防線。
線上工具推薦
- /zh-TW/json/format - JSON格式化,除錯品牌型別序列化輸出
- /zh-TW/dev/curl-to-code - API請求轉程式碼,品牌型別介面除錯
- /zh-TW/encode/hash - 雜湊計算,品牌ID唯一性校驗
- /zh-TW/text/diff - 文字對比,對比品牌型別定義變更
本站提供瀏覽器本地工具,免註冊即可試用 →
#TypeScript Brand Type#类型安全#nominal typing#类型品牌#TypeScript 5.5+#2026#前端开发