函数与泛型
本章涵盖 TypeScript 函数声明、参数类型、函数重载,以及泛型函数、泛型约束、泛型接口等核心泛型特性,帮助你掌握 TS 类型系统中与函数相关的高级能力。
函数类型声明
TypeScript 中可以为函数参数和返回值添加类型注解。箭头函数在参数列表和函数体之间必须有 =>。
// 箭头函数
const add = (a: number, b: number): number => {
return a + b
}
// 具名函数
function multiply(a: number, b: number): number {
return a * b
}
// 无返回值
function log(message: string): void {
console.log(message)
}函数类型别名
可以用 type 定义函数类型别名,将类型与实现分离,便于复用和作为回调参数传递:
type MathOp = (a: number, b: number) => number
const add: MathOp = (a, b) => a + b // ✅ 实参类型自动推断
const multiply: MathOp = (a, b) => a * b
// 作为回调参数
function calculate(op: MathOp, x: number, y: number): number {
return op(x, y)
}参数
可选参数
在参数名后加 ? 表示可选参数。可选参数必须放在必选参数之后,未传入时值为 undefined。
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name
}
greet("Willow") // ✅ "Willow"
greet("Willow", "Dr.") // ✅ "Dr. Willow"
🚨 陷阱:可选参数不可与默认值参数同时使用
?— 有默认值的参数本身就是可选的,加?会产生冲突。可选参数必须位于参数列表末尾(除 rest 参数外)。
默认参数
默认值参数自动获得可选特性,但会覆盖 undefined(传入的 undefined 会触发默认值)。
// ✅ 正确写法
const greeting = (name: string, age: number = 18): string => {
return `I am ${name}, and age is ${age} years old.`
}
greeting("Willow") // I am Willow, and age is 18 years old.
// ❌ 错误:不能同时使用 ? 和默认值
// function bad(param?: string = "default") {}
Rest 参数
Rest 参数必须是参数列表的最后一个,类型标注为数组:
function sum(a: number, b: number, ...rest: number[]): number {
return rest.reduce((s, n) => s + n, a + b)
}
sum(1, 2) // 3
sum(1, 2, 3, 4, 5) // 15
函数重载
当函数的参数类型或数量不同导致返回值类型也不同时,使用函数重载为每种调用方式提供精确的类型定义。
重载签名
重载签名是对函数多种调用方式的类型声明,只定义参数签名与返回值签名,没有函数体实现:
function getId(id: string): string
function getId(id: number): number- 重载签名是调用方看到的接口,IDE 跳转和代码提示都会定位到重载签名
- TS 从第一个重载签名开始尝试匹配,向下查找第一个兼容的签名
实现签名
实现签名提供统一的函数体,参数和返回值必须使用能涵盖所有重载签名的宽泛类型:
// 重载签名(两个,无函数体)
function getId(id: string): string
function getId(id: number): number
// 实现签名(一个,宽泛类型)
function getId(id: unknown): unknown {
if (typeof id === "string") {
return id
}
return id
}
getId("willow") // 返回 string 类型 ✅
getId(42) // 返回 number 类型 ✅
🚨 陷阱:实现签名使用宽泛类型(如
unknown),调用方只能看到重载签名。重载签名顺序很重要 — 具体在前、宽泛在后。实现签名本身对外部不可见。
如果实现签名类型不够宽泛,会导致类型错误:
// ❌ 错误:实现签名返回值是 string,无法匹配第二个重载签名(返回 number)
function getId(id: string): string
function getId(id: number): number
// function getId(id: unknown): string { ... } // ❌ 类型错误
💡 最佳实践:重载签名按从具体到宽泛的顺序排列,最精确的匹配放在最前面。
泛型函数
泛型允许函数在定义时不指定具体类型,而在调用时由传入参数推断或显式指定。这使得函数可以处理多种类型,同时保持类型安全。
function identity<T>(arg: T): T {
return arg
}
// 类型推断:TS 根据传入值自动推断 T
const result = identity("hello") // const result: "hello"
const count = identity(42) // const count: 42
// 显式指定类型参数
const num = identity<number>(42) // const num: number
const str = identity<string>("hi") // const str: string
T 是类型变量,命名惯例使用单大写字母。常见命名:
| 字母 | 含义 |
|---|---|
T |
Type(通用类型) |
K |
Key(键类型) |
V |
Value(值类型) |
E |
Element(元素类型) |
R |
Return(返回值类型) |
泛型约束
使用 extends 关键字限制类型参数必须满足某个结构或接口,从而在函数体内安全地访问特定属性:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest("hello", "world") // ✅ string 有 length
longest([1, 2], [3, 4, 5]) // ✅ array 有 length
// longest(1, 2) // ❌ Error: number 没有 length
使用 keyof 约束
keyof 获取对象类型的键的联合类型,常用于安全地访问对象属性:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: "Willow", age: 28, email: "w@example.com" }
getProperty(user, "name") // ✅ 返回 string
getProperty(user, "age") // ✅ 返回 number
// getProperty(user, "phone") // ❌ Error: "phone" 不是 user 的键
泛型默认值
可以为泛型参数设置默认类型,调用方不指定时使用默认值:
function createBox<T = string>(): { contents: T } {
return { contents: undefined as unknown as T }
}
const stringBox = createBox() // { contents: string }
const numBox = createBox<number>() // { contents: number }
多个类型参数
泛型函数可以接受多个类型参数,实现灵活的依赖关系:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second]
}
const p1 = pair("hello", 42) // [string, number]
const p2 = pair(true, [1, 2, 3]) // [boolean, number[]]
// 常见组合:K(键)、V(值)
function getOrDefault<K, V>(map: Map<K, V>, key: K, defaultVal: V): V {
return map.has(key) ? map.get(key)! : defaultVal
}泛型接口与泛型类(简介)
泛型同样适用于接口和类,实现高复用的容器和数据结构。详细内容参见后续章节。
// 泛型接口
interface Repository<T> {
getById(id: string): T
save(entity: T): void
list(): T[]
}
// 泛型类
class Stack<T> {
private items: T[] = []
push(item: T): void { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
peek(): T | undefined { return this.items[this.items.length - 1] }
}
const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
console.log(numStack.pop()) // 2
this 参数
TypeScript 允许将 this 作为函数的第一个参数进行类型声明,该参数是编译时标注,不会出现在编译后的 JavaScript 中:
function handleClick(this: HTMLButtonElement, event: MouseEvent): void {
console.log(this.textContent) // this 类型安全
}
const btn = document.querySelector("button")!
// btn.addEventListener("click", handleClick) // ✅ this 正确
// btn.addEventListener("click", e => handleClick(e)) // ❌ this 丢失
💡 最佳实践:使用
this参数避免回调中的this丢失问题。它是纯编译时的类型辅助,不会生成任何运行时代码,也不计入实际参数个数。
如果 this 类型与调用上下文不匹配,TS 会在编译期报错:
function getName(this: { name: string }): string {
return this.name
}
const obj = { name: "Willow", getName }
obj.getName() // ✅ this 匹配
const standalone = obj.getName
// standalone() // ❌ Error: 'this' context is not assignable
调用签名与构造签名
调用签名(Call Signature)
用于描述一个可作为函数调用的对象,同时该对象还可能带有额外属性:
interface Callable {
(x: number): string
description: string
}
function createCallable(): Callable {
const fn = (x: number): string => `Value: ${x}`
fn.description = "A callable number-to-string converter"
return fn
}
const c = createCallable()
c(42) // "Value: 42"
c.description // "A callable number-to-string converter"
构造签名(Construct Signature)
描述可以用 new 调用的类型,常见于工厂模式或依赖注入:
interface Constructable<T> {
new (name: string): T
}
class Person {
constructor(public name: string) {}
}
function factory<T>(ctor: Constructable<T>, name: string): T {
return new ctor(name)
}
const p = factory(Person, "Willow") // Person 实例
常见陷阱汇总
| 陷阱 | 说明 | 正确做法 |
|---|---|---|
void 与 undefined 混淆 |
void 表示忽略返回值,但运行时可能返回 undefined;void 类型变量只能赋值为 undefined |
区分编译时语义和运行时行为,不要依赖 void 返回值的具体内容 |
| Rest 参数位置错误 | Rest 参数必须放在参数列表最后,且只能有一个 | function fn(a: number, ...rest: number[]): void {} |
| 重载签名顺序不当 | 宽泛签名在前会吞掉后续具体签名的匹配 | 具体签名在前,宽泛签名在后 |
| 泛型约束过紧 | 使用 extends 约束了不必要的属性,导致函数适用范围变窄 |
只约束函数体内实际访问的属性,使用 extends { length: number } 而非 extends string |
| 类型参数推断混淆 | 多个类型参数时,只部分显式指定会导致推断失败 | 要么全部推断,要么全部显式指定;或考虑拆分函数 |