数组、元组、对象、索引签名与只读边界
TypeScript 对象类型描述“值至少有哪些成员”。这是一套结构化模型,不会在运行时创建 schema。
1. 数组与只读数组
const scores: number[] = [92, 87, 100]
const names: Array<string> = ["Ada", "Linus"]
const readonlyScores: readonly number[] = [92, 87]
scores.push(95)
// readonlyScores.push(95) // 编译错误
readonly number[] 禁止通过该引用修改数组,但原始可变数组若还有别名,仍可被修改:
const mutable = [1, 2]
const view: readonly number[] = mutable
mutable.push(3)
console.log(view.length) // 3
因此 readonly 是能力限制,不是深冻结或拷贝。
2. 元组
type Point = readonly [x: number, y: number]
type Result<T, E> = readonly [ok: true, value: T] | readonly [ok: false, error: E]
const point: Point = [10, 20]
const result: Result<number, string> = [true, 42]
元组适合位置语义稳定、元素少的返回值。字段多或含义可能演进时优先对象,避免调用方记忆下标。
可选与剩余元素
type Command = [program: string, cwd?: string, ...args: string[]]
const command: Command = ["git", "/repo", "status", "--short"]
3. 对象属性修饰符
interface UserProfile {
readonly id: string
displayName: string
avatarUrl?: URL
}
function rename(profile: UserProfile, name: string): UserProfile {
return { ...profile, displayName: name }
}
readonly阻止通过该类型写入属性。?表示属性可能缺少;在exactOptionalPropertyTypes下,“缺少”与“显式赋undefined”不再自动等同。
interface ExactOptions {
color?: "light" | "dark"
}
const a: ExactOptions = {}
// const b: ExactOptions = { color: undefined } // 开启 exactOptionalPropertyTypes 后报错
若确实允许显式 undefined,写为 color?: "light" | "dark" | undefined。
4. 索引签名与安全索引
type Inventory = {
[sku: string]: number
}
const inventory: Inventory = { book: 3 }
const count = inventory["missing"] // 开启 noUncheckedIndexedAccess 后为 number | undefined
动态键集合常可用 Map 或限定键的 Record 表达得更准确:
type Locale = "zh-CN" | "zh-TW" | "en"
const labels: Record<Locale, string> = {
"zh-CN": "语言",
"zh-TW": "語言",
en: "Language",
}
新增联合成员时,Record 会要求补全对应键。
5. 多余属性检查与结构兼容
type Point2D = { x: number; y: number }
function lengthOf(point: Point2D): number {
return Math.hypot(point.x, point.y)
}
const point3D = { x: 3, y: 4, z: 5 }
lengthOf(point3D) // 结构兼容
// lengthOf({ x: 3, y: 4, z: 5 })
// 新鲜对象字面量触发多余属性检查
这项差异用来捕捉对象字面量拼写错误;不要用中间变量“绕过”真实建模问题。
6. 深只读与运行时不可变
type DeepReadonly<T> =
T extends (...args: never[]) => unknown ? T :
T extends readonly unknown[] ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T
上述是静态递归类型,仍不会冻结对象。Object.freeze 是浅层运行时操作;深冻结需要递归实现,并注意 Map、Set、日期、循环引用与性能。
7. 对象建模清单
- 是否真的允许缺少属性,还是值允许为空?
- 键集合是开放字符串,还是封闭联合?
- 调用方需要写权限吗?
- 是否应该返回新对象而非修改参数?
- 外部对象是否已经运行时校验?
Record<string, T>的缺失键是否通过noUncheckedIndexedAccess体现?
参考:Object Types、TSConfig: exactOptionalPropertyTypes、TSConfig: noUncheckedIndexedAccess。