TypeScript 日常类型:推断、函数、数组与元组
TypeScript 日常类型:推断、函数、数组与元组
系列导航:TypeScript 7 现代开发指南
上一篇:工程起步
下一篇:类型收窄
日常 TypeScript 不需要给每个变量都写类型。编译器会从初始值、函数返回值和上下文推断类型;开发者更应该在函数边界、公开 API 和需要约束设计意图的位置写清契约。
本文从最常用的原始类型开始,覆盖字面量、函数、数组、只读数组、元组、空值以及容易误解的 void。
一、优先让推断完成局部工作
初始化值足够明确时,不必重复类型:
const title = 'TypeScript 日常类型' // string
const views = 120 // number
const published = false // boolean
const article = {
title,
views,
published,
}
下面的注解没有增加信息:
const count: number = 1
const enabled: boolean = true
需要显式注解的典型位置包括:
- 函数参数和公开返回值;
- 变量先声明、后赋值;
- 值需要比初始化结果更宽或更窄;
- 对象需要满足某个稳定契约;
- 空数组或空对象没有足够的推断信息。
let currentUserId: string | null = null
function loadArticle(id: string): Promise<string> {
return Promise.resolve(`article:${id}`)
}
推断并非“复杂时就不可靠”。只要输入关系清楚,TypeScript 很擅长推断复杂对象和泛型结果;盲目添加注解反而可能拓宽本来精确的类型。
二、原始类型使用小写名称
常用原始类型是:
const name: string = 'Ada'
const age: number = 36
const active: boolean = true
const total: bigint = 10n
const token: symbol = Symbol('token')
日常类型声明使用小写 string、number、boolean,不要使用包装对象类型 String、Number、Boolean:
let text: string = 'hello'
// text = new String('hello')
// 错误:包装对象不是原始 string
JavaScript 会在访问 'hello'.toUpperCase() 时临时提供包装行为,不意味着业务代码应该主动创建包装对象。
三、字面量类型表达有限取值
字面量可以直接成为类型:
type Theme = 'light' | 'dark' | 'system'
type RetryCount = 0 | 1 | 2 | 3
function setTheme(theme: Theme): void {
console.log(theme)
}
setTheme('dark')
const 变量通常保留更具体的字面量,let 为后续赋值保留空间:
const fixedMode = 'production' // 'production'
let mode = 'production' // string
mode = 'development'
对象属性默认仍可能变化。需要保留整组常量的字面量时使用 as const:
const Status = {
Draft: 'draft',
Published: 'published',
} as const
type Status = (typeof Status)[keyof typeof Status]
四、函数参数和返回值
参数通常需要注解,返回值可以推断;公开 API 显式写返回值有助于防止实现变化意外改变契约:
function sum(left: number, right: number): number {
return left + right
}
函数类型使用箭头表示参数和返回值:
type Formatter = (value: number) => string
const formatPrice: Formatter = (value) => `¥${value.toFixed(2)}`
可选参数放在必选参数之后,调用时必须处理 undefined:
function greet(name: string, prefix?: string): string {
return `${prefix ?? '你好'},${name}`
}
默认参数在函数体内已经有值:
function repeat(text: string, times = 1): string {
return text.repeat(times)
}
剩余参数使用数组类型:
function join(separator: string, ...parts: string[]): string {
return parts.join(separator)
}
join('/', 'posts', 'typescript', '7')
五、数组和只读数组
数组有两种等价写法:
const names: string[] = ['Ada', 'Linus']
const scores: Array<number> = [95, 88]
函数只读取数组时,用 readonly T[] 表达不会修改调用方数据:
function first<T>(items: readonly T[]): T | undefined {
return items[0]
}
const tags: readonly string[] = ['TypeScript', 'JavaScript']
// tags.push('Web') // 错误:只读数组没有 push
readonly 是浅层类型约束,不会运行时冻结数组,也不会自动让元素对象的内部属性只读。
开启 noUncheckedIndexedAccess 后,数组索引会考虑越界:
const values = [10, 20]
const maybeValue = values[5] // number | undefined
这比默认假设每次索引都存在更符合 JavaScript 的运行时事实。
六、元组描述固定位置
元组适合长度和每个位置语义都稳定的数据:
type Coordinate = readonly [x: number, y: number]
const point: Coordinate = [120.1, 30.2]
可选元素和剩余元素也受支持:
type Command = [name: string, verbose?: boolean]
type Route = [path: string, ...middlewares: string[]]
const build: Command = ['build', true]
const admin: Route = ['/admin', 'auth', 'audit']
如果元素数量会自由变化,应该使用普通数组;如果每个字段需要名称、可选性和未来扩展,使用对象通常比很长的元组更清楚。
七、null、undefined 与可选链
在严格模式下,null 和 undefined 不能直接赋给其他类型:
function findName(id: string): string | undefined {
return id === 'u_001' ? 'Ada' : undefined
}
const upperName = findName('u_002')?.toUpperCase() ?? 'UNKNOWN'
用联合类型表达缺失,用可选链 ?. 安全访问,用空值合并 ?? 提供默认值。不要用 value || fallback 代替所有空值处理,因为 0、false 和空字符串也会被 || 当作假值。
八、正确理解 void
函数返回类型 void 表示调用者不应使用返回值:
function log(message: string): void {
console.log(message)
}
它不等于“实现绝不能返回任何东西”。当一个函数被赋给 () => void 类型时,实现可以返回值,但调用方看到的仍是 void:
type Listener = (message: string) => void
const messages: string[] = []
const listener: Listener = (message) => messages.push(message)
const result = listener('hello')
// result 的类型是 void,调用方不能依赖 push 返回的长度
这个规则让 forEach((item) => output.push(item)) 等回调自然成立。它不意味着显式声明 function run(): void 后可以随意返回数字;函数声明本身的返回类型仍会检查实现。
九、函数重载只在调用形态确实不同时使用
同一函数有几个关联的调用签名时可以重载:
function parse(value: string): string[]
function parse(value: readonly string[]): string
function parse(value: string | readonly string[]): string[] | string {
return typeof value === 'string' ? value.split(',') : value.join(',')
}
const list = parse('a,b') // string[]
const text = parse(['a', 'b']) // string
实现签名必须覆盖所有重载,但不会直接暴露给调用方。简单的“参数可以是 A 或 B,返回值不随输入变化”优先使用联合类型,避免不必要的重载。
十、常见误区
- 给每个局部变量写类型:重复注解增加噪声,还可能丢失字面量精度。
- 使用大写包装类型:业务声明应使用小写原始类型。
- 把
const当深只读:它只禁止变量重新赋值,对象和数组内容仍可能变化。 - 用长元组代替对象:位置语义难维护时,应改用具名属性。
- 忽略数组越界:对动态索引开启
noUncheckedIndexedAccess,并处理undefined。 - 依赖
void回调的实现返回值:调用方契约明确忽略它。
小结
让 TypeScript 推断局部实现,在函数边界和公开契约处明确类型。原始类型使用小写名称,有限状态用字面量联合;数组长度可变,元组位置固定,纯读取输入使用只读数组。严格空值检查和 noUncheckedIndexedAccess 能让类型更贴近运行时,而 void 表达的是“调用者忽略返回值”的契约。