函数的类型契约:从参数校验到调用安全
需求场景
订单系统进入核心开发阶段。你需要编写一系列工具函数:计算折扣价格、格式化订单摘要、校验库存数量、处理异步支付回调。这些函数将被团队中多个开发者调用。
如果函数没有明确的参数和返回值约定,调用方只能靠函数名猜测用法,猜错就是运行时 bug。TypeScript 的函数类型系统让每个函数都带上一份精确的「调用协议」——参数必须传什么、返回什么、哪些参数可以省略——编译器替你检查每一次调用是否合规。
函数声明的类型标注
基本形式
在函数声明中,参数名后面加 :类型,参数列表后面加 :返回类型:
function calculateTotal(price: number, qty: number): number {
return price * qty;
}
调用时,参数的个数和类型必须严格匹配:
calculateTotal(29.9, 3); // 正确,返回 89.7
calculateTotal(29.9, 3, 0.1); // 编译错误:参数过多
calculateTotal(29.9); // 编译错误:参数不足
calculateTotal(29.9, "three"); // 编译错误:类型不匹配
函数表达式
赋值给变量的函数有两种类型标注方式。
让编译器从右侧推断:
const calculateTotal = function (price: number, qty: number): number {
return price * qty;
};
显式标注变量的函数类型:
const calculateTotal: (price: number, qty: number) => number =
function (price, qty) {
return price * qty;
};
类型声明中的 => 是 TypeScript 的函数类型语法,表示「输入 -> 输出」的映射关系。它和 ES6 箭头函数的 => 是完全不同的概念:前者出现在类型上下文,后者出现在值上下文。
使用 type 提取函数类型
当多个函数共享相同的签名时,用 type 定义函数类型别名可以减少重复:
type PricingRule = (basePrice: number, qty: number) => number;
const standardPricing: PricingRule = (base, qty) => base * qty;
const vipPricing: PricingRule = (base, qty) => base * qty * 0.85;
const wholesalePricing: PricingRule = (base, qty) =>
qty >= 100 ? base * qty * 0.7 : base * qty;
修改签名时只需改一处,所有使用该类型的地方同步生效。
使用 interface 定义
接口也可以描述函数的签名,特别适合函数本身还需要附带属性的场景:
interface Validator {
(input: string): boolean;
errorMessage: string;
reset(): void;
}
function createValidator(msg: string): Validator {
let fn = function (input: string) {
return input.length > 0;
} as Validator;
fn.errorMessage = msg;
fn.reset = function () {};
return fn;
}
Function 类型
TypeScript 提供了一个通用的 Function 类型,但它不携带任何参数和返回值信息,实质上等于关闭了类型检查:
function invoke(fn: Function) {
return fn(1, 2, 3); // 参数和返回值都是 any
}
如果确实需要表示「任意函数」,更安全的写法是:
type AnyFn = (...params: any[]) => any;
参数与返回值
参数类型
function generateInvoice(orderId: string, amount: number): void {
console.log(`Invoice for ${orderId}: ${amount}`);
}
在回调场景中,参数类型可以从上下文推断:
[10, 20, 30].forEach(val => {
console.log(val.toFixed(2)); // val 自动推断为 number
});
返回值类型
大多数场景下返回值类型可以省略,编译器根据 return 语句推断:
function computeTax(amount: number) {
return amount * 0.13; // 推断返回 number
}
建议显式标注的场景:
- 对外暴露的公共 API
- 函数体有多个 return 分支且类型不同
- 返回类型与直觉不一致时
参数解构
type ShippingParams = {
weight: number;
distance: number;
express: boolean;
};
function calcShipping({ weight, distance, express }: ShippingParams): number {
const base = weight * 0.5 + distance * 0.02;
return express ? base * 1.5 : base;
}
calcShipping({ weight: 2.5, distance: 800, express: true });
数组解构同理:
function firstItem([initial, ...remaining]: number[]): number {
return initial;
}
可选参数与默认值
可选参数
用 ? 标记的参数可以不传,其实际类型是 声明类型 | undefined:
function createOrder(
productId: string,
qty: number,
couponCode?: string
): string {
let summary = `${productId} x ${qty}`;
if (couponCode !== undefined) {
summary += ` (coupon: ${couponCode})`;
}
return summary;
}
createOrder("P-001", 2); // "P-001 x 2"
createOrder("P-001", 2, "SAVE10"); // "P-001 x 2 (coupon: SAVE10)"
可选参数必须位于必需参数之后。 将可选参数放在前面会导致编译错误。
使用空值合并运算符 ?? 可以简化可选参数的处理:
function greetCustomer(name: string, title?: string): string {
const prefix = title ?? "Customer";
return `Welcome, ${prefix} ${name}`;
}
参数默认值
带默认值的参数自动被视为可选参数,且不受「必须在末尾」的限制:
function formatPrice(
amount: number,
currency: string = "CNY",
decimals: number = 2
): string {
return `${currency} ${amount.toFixed(decimals)}`;
}
formatPrice(99); // "CNY 99.00"
formatPrice(99, "USD"); // "USD 99.00"
formatPrice(99, "EUR", 0); // "EUR 99"
注意:可选标记 ? 和默认值不能同时使用。
可选属性的类型本质
function applyLabel(
text: string,
prefix?: string
): string {
// prefix 的类型是 string | undefined
if (prefix !== undefined) {
return `[${prefix}] ${text}`;
}
return text;
}
剩余参数
当函数需要接收不定数量的参数时,使用 ... 收集:
function mergeIds(separator: string, ...ids: string[]): string {
return ids.join(separator);
}
mergeIds("-", "A01", "B02", "C03"); // "A01-B02-C03"
剩余参数只能是最后一个参数。其类型也可以是元组,用于精确控制参数的数量和类型:
function registerEvent(
...args: [target: string, action: string, priority: number]
): void {
const [target, action, priority] = args;
console.log(`${target}.${action} @ priority ${priority}`);
}
registerEvent("button", "click", 1);
registerEvent("button", "click"); // 编译错误:缺少参数
函数重载
为什么需要重载
某些函数根据传入参数的类型不同,返回不同类型的结果。用联合类型可以实现,但返回类型会变得模糊。函数重载让编译器能精确追踪「输入A -> 输出A、输入B -> 输出B」的对应关系。
语法结构
多个重载签名 + 一个实现签名:
function parseInput(raw: string): string[];
function parseInput(raw: number): number;
function parseInput(raw: string | number): string[] | number {
if (typeof raw === "string") {
return raw.split(",");
}
return raw * 2;
}
const words = parseInput("a,b,c"); // 类型为 string[]
const doubled = parseInput(5); // 类型为 number
重载签名按顺序从上到下匹配,精确的定义应写在前面。实现签名对外部不可见。
重载 vs 联合类型
对于所有输入共享相同返回类型的场景,联合类型更简洁:
// 联合类型足够
function measure(target: string | string[]): number {
return Array.isArray(target) ? target.length : target.length;
}
当输入与输出之间存在一对一映射关系时,重载提供更精确的类型推导。
实际应用
function findElement(selector: "input"): HTMLInputElement;
function findElement(selector: "canvas"): HTMLCanvasElement;
function findElement(selector: "video"): HTMLVideoElement;
function findElement(selector: string): HTMLElement;
function findElement(selector: string): HTMLElement {
return document.querySelector(selector)!;
}
const input = findElement("input"); // HTMLInputElement
const canvas = findElement("canvas"); // HTMLCanvasElement
const div = findElement("div"); // HTMLElement
箭头函数的类型
箭头函数的类型标注方式与普通函数一致:
const multiply = (a: number, b: number): number => a * b;
const notify = (msg: string): void => {
console.log(`[Notification] ${msg}`);
};
当箭头函数赋值给已标注类型的变量时,参数类型可以省略:
type Comparator = (x: number, y: number) => number;
const ascending: Comparator = (x, y) => x - y;
const descending: Comparator = (x, y) => y - x;
回调函数类型
基本回调
function fetchOrderDetail(
orderId: string,
onComplete: (detail: string) => void
): void {
const result = `Detail of ${orderId}`;
onComplete(result);
}
fetchOrderDetail("ORD-100", (detail) => {
console.log(detail.toUpperCase()); // detail 推断为 string
});
带错误处理的回调
type ResultCallback = (err: Error | null, data?: string) => void;
function loadConfig(path: string, cb: ResultCallback): void {
try {
const content = `config from ${path}`;
cb(null, content);
} catch (e) {
cb(e as Error);
}
}
参数个数的灵活性
TypeScript 允许回调函数的实际参数少于声明参数:
type FullCallback = (val: number, idx: number, src: number[]) => void;
const simpleCb: FullCallback = (val) => console.log(val);
const pairCb: FullCallback = (val, idx) => console.log(val, idx);
这与 JavaScript 的回调习惯一致——forEach 的回调可以只接收当前元素,不必声明 index 和 array。
this 类型声明
TypeScript 允许在参数列表中显式声明 this 的类型。这是一个「幽灵参数」,不占据实际参数位置:
interface OrderProcessor {
orderId: string;
process(this: OrderProcessor): void;
}
const processor: OrderProcessor = {
orderId: "ORD-200",
process() {
console.log(`Processing ${this.orderId}`);
}
};
processor.process(); // 正确
const detached = processor.process;
detached(); // 编译错误:this 上下文不匹配
这可以防止方法被从对象上解绑后在错误的上下文中调用。
void 返回类型的特殊规则
基本行为
返回类型为 void 的函数不应返回有意义的值:
function audit(action: string): void {
console.log(`Audit: ${action}`);
}
宽容性设计
当函数类型被声明为返回 void 时,该类型变量可以接收实际有返回值的函数。但通过该变量调用后,返回值的类型仍然是 void,不能被使用:
type VoidFn = () => void;
const fn: VoidFn = () => 42; // 不报错
const result = fn(); // result 的类型是 void
const doubled = result * 2; // 编译错误
这个设计的原因是实用性。考虑常见场景:
const collected: number[] = [];
[10, 20, 30].forEach(n => collected.push(n));
// forEach 的回调声明为返回 void
// 但 push 实际返回数组的新长度(number)
// 如果严格禁止这种写法,大量现有代码都需要修改
void vs undefined vs never
| 返回类型 | 含义 | 函数能否正常结束 |
|---|---|---|
void | 不关心返回值 | 能 |
undefined | 显式返回 undefined | 能 |
never | 永远无法结束 | 不能 |
function log(msg: string): void { console.log(msg); }
function noop(): undefined { return undefined; }
function crash(msg: string): never { throw new Error(msg); }
高阶函数
返回函数的函数:
function createFormatter(prefix: string): (val: number) => string {
return (val: number) => `${prefix}${val.toFixed(2)}`;
}
const formatCNY = createFormatter("CNY ");
const formatUSD = createFormatter("$ ");
formatCNY(99.5); // "CNY 99.50"
formatUSD(99.5); // "$ 99.50"
用类型别名可以让高阶函数的签名更清晰:
type MoneyFormatter = (val: number) => string;
type FormatterFactory = (prefix: string) => MoneyFormatter;
const createFormatter: FormatterFactory = (prefix) => {
return (val) => `${prefix}${val.toFixed(2)}`;
};
只读参数
当函数不应该修改传入的数据时,使用 readonly 修饰参数:
function sumAll(values: readonly number[]): number {
// values.push(0); // 编译错误
// values[0] = 99; // 编译错误
return values.reduce((acc, v) => acc + v, 0);
}
const prices = [10, 20, 30];
sumAll(prices); // 60,prices 不会被修改
这是一种清晰的意图表达:这个函数只读取数据,不产生副作用。
构造函数类型
描述可以通过 new 调用的函数:
class OrderItem {
constructor(
public sku: string,
public qty: number
) {}
}
type ItemFactory = new (sku: string, qty: number) => OrderItem;
function buildItem(Factory: ItemFactory, sku: string, qty: number): OrderItem {
return new Factory(sku, qty);
}
const item = buildItem(OrderItem, "SKU-100", 5);
局部类型声明
函数内部可以定义只在函数体内可用的类型:
function processApiResponse(raw: string) {
type ParsedPayload = {
code: number;
body: string;
};
const payload: ParsedPayload = JSON.parse(raw);
return payload.code;
}
// ParsedPayload 在函数外不可用
这可以避免全局类型命名空间的污染。
综合示例:订单处理管线
type StepResult = { success: boolean; data?: string };
type PipelineStep = (input: string) => StepResult;
function createPipeline(...steps: PipelineStep[]) {
return function execute(initialInput: string): StepResult {
let current = initialInput;
for (const step of steps) {
const outcome = step(current);
if (!outcome.success) {
return outcome;
}
current = outcome.data ?? current;
}
return { success: true, data: current };
};
}
const validateOrder: PipelineStep = (input) => {
return input.length > 0
? { success: true, data: input }
: { success: false, data: "Empty input" };
};
const enrichOrder: PipelineStep = (input) => {
return { success: true, data: `${input} [enriched]` };
};
const pipeline = createPipeline(validateOrder, enrichOrder);
const outcome = pipeline("ORD-500");
console.log(outcome); // { success: true, data: "ORD-500 [enriched]" }
这个示例综合运用了:
type定义函数类型别名- 剩余参数收集多个处理步骤
- 高阶函数返回可执行的管线
- 可选属性
data?的使用
本章回顾
| 知识点 | 核心内容 |
|---|---|
| 函数声明类型 | 参数后加 :类型,参数列表后加 :返回类型 |
| 函数类型别名 | type Fn = (a: T) => R,一处定义多处复用 |
| 可选参数 | param?: Type,必须在必需参数之后 |
| 默认值参数 | 自动可选,不受位置限制 |
| 剩余参数 | ...args: Type[],只能是最后一个参数 |
| 函数重载 | 多个签名 + 一个实现,精确的写前面 |
| 回调函数 | 实际参数可以少于声明参数 |
| this 声明 | 幽灵参数,防止方法脱离上下文调用 |
| void 宽容性 | void 类型变量可接收有返回值的函数 |
| 只读参数 | readonly 防止函数内部修改传入数据 |
| 构造函数类型 | new (...) => T 描述可实例化的类型 |
购买课程解锁全部内容
告别类型错误:12 章掌握 TypeScript 工程实战
¥29.90