跳到主要内容
预计阅读 30 分钟

函数的类型契约:从参数校验到调用安全

需求场景

订单系统进入核心开发阶段。你需要编写一系列工具函数:计算折扣价格、格式化订单摘要、校验库存数量、处理异步支付回调。这些函数将被团队中多个开发者调用。

如果函数没有明确的参数和返回值约定,调用方只能靠函数名猜测用法,猜错就是运行时 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 的回调可以只接收当前元素,不必声明 indexarray

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