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')

日常类型声明使用小写 stringnumberboolean,不要使用包装对象类型 StringNumberBoolean

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']

如果元素数量会自由变化,应该使用普通数组;如果每个字段需要名称、可选性和未来扩展,使用对象通常比很长的元组更清楚。

七、nullundefined 与可选链

在严格模式下,nullundefined 不能直接赋给其他类型:

function findName(id: string): string | undefined {
  return id === 'u_001' ? 'Ada' : undefined
}

const upperName = findName('u_002')?.toUpperCase() ?? 'UNKNOWN'

用联合类型表达缺失,用可选链 ?. 安全访问,用空值合并 ?? 提供默认值。不要用 value || fallback 代替所有空值处理,因为 0false 和空字符串也会被 || 当作假值。

八、正确理解 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,返回值不随输入变化”优先使用联合类型,避免不必要的重载。

十、常见误区

  1. 给每个局部变量写类型:重复注解增加噪声,还可能丢失字面量精度。
  2. 使用大写包装类型:业务声明应使用小写原始类型。
  3. const 当深只读:它只禁止变量重新赋值,对象和数组内容仍可能变化。
  4. 用长元组代替对象:位置语义难维护时,应改用具名属性。
  5. 忽略数组越界:对动态索引开启 noUncheckedIndexedAccess,并处理 undefined
  6. 依赖 void 回调的实现返回值:调用方契约明确忽略它。

小结

让 TypeScript 推断局部实现,在函数边界和公开契约处明确类型。原始类型使用小写名称,有限状态用字面量联合;数组长度可变,元组位置固定,纯读取输入使用只读数组。严格空值检查和 noUncheckedIndexedAccess 能让类型更贴近运行时,而 void 表达的是“调用者忽略返回值”的契约。

官方资料

系列导航:目录 · 上一篇 · 下一篇