函数类型全解
参数、返回值、重载、类型谓词——函数是 TS 类型的核心战场
🎯 学习目标
学完本章,你将能够:
- 能够正确标注函数参数类型、返回类型与可选/默认参数
- 能够使用泛型函数实现类型安全的复用逻辑
- 能够理解函数重载的声明规则与解析顺序
- 能够标注 this 参数类型与回调函数类型
- 能够运用类型谓词(is)和断言函数(asserts)实现类型收窄
函数的类型
JS 函数不告诉你参数该传什么、返回什么,TS 来解决
// JS:参数类型、返回类型都靠猜
function add(a, b) {
return a + b;
}
add(1, 2); // 3
add("1", "2"); // "12" —— 静默出错
// TS:参数和返回值都有明确类型
function add(a: number, b: number): number {
return a + b;
}
add(1, 2); // 3
// add("1", "2"); // 编译报错!
Part 1: 参数与返回类型
函数类型标注最核心的场景:每个参数和返回值都可以标注类型
上一章学了变量和数组的类型注解,这一章进入函数——TS 类型系统的"主战场"。一个函数的参数该传什么类型?返回什么类型?void、undefined、never 这些返回类型各自什么场景用?这一节从最基本的参数和返回值标注开始,打好函数类型的基础。
基本标注方式
参数类型 + 返回值类型
// 每个参数都标注类型,返回值也标注
function add(a: number, b: number): number {
return a + b;
}
// 返回值类型通常可以省略(TS 自动推断)
function addInferred(a: number, b: number) {
return a + b; // 推断返回 number
}
// 无返回值的函数使用 void
function log(message: string): void {
console.log(message);
}
// 永远不会返回的函数使用 never
function throwError(msg: string): never {
throw new Error(msg);
}
三种返回类型
void、undefined、never 各有用途
| 返回类型 | 含义 | 适用场景 |
|---|---|---|
void | 不返回有意义的值 | console.log、事件处理 |
undefined | 返回 undefined 这个值 | 需要显式 return undefined |
never | 永远不会正常结束 | 抛异常、无限循环 |
对象参数标注
内联类型 vs 解构标注
// 写法1:内联类型
function printUser(user: { name: string; age: number }): void {
console.log(`${user.name}, ${user.age}岁`);
}
// 写法2:解构 + 类型(推荐)
function printUser({ name, age }: { name: string; age: number }): void {
console.log(`${name}, ${age}岁`);
}
// 调用
printUser({ name: "张三", age: 25 });
解构标注的好处:函数体里直接用 name 而不是 user.name,代码更简洁。
Part 2: 可选与默认参数
让函数调用更灵活,但类型行为有微妙区别
不是每次调用函数都需要传所有参数。有时问候语用默认值就行,有时某些参数可以省略。TS 提供了两种方式:? 标记可选参数,= 设默认值。两者看起来很像,但函数体内的类型行为完全不同。这一节搞清楚它们的区别,以及为什么推荐优先用默认参数。
可选参数
用 ? 标记,调用时可以省略
// 可选参数用 ? 标记
function greet(name: string, greeting?: string): string {
return `${greeting ?? "你好"}, ${name}!`;
}
greet("张三"); // "你好, 张三!"
greet("张三", "早上好"); // "早上好, 张三!"
function bad(greeting?: string, name: string) 会报错。
默认参数
用 = 指定默认值,推荐优先使用
// 默认参数用 = 指定默认值
function greet(name: string, greeting: string = "你好"): string {
return `${greeting}, ${name}!`;
}
greet("张三"); // "你好, 张三!"
greet("张三", "早上好"); // "早上好, 张三!"
greet("张三", undefined); // "你好, 张三!" —— 显式传 undefined 触发默认值
可选 vs 默认
优先用默认参数,函数体内类型更确定
可选参数 ?
function f(x?: number) {
// x: number | undefined
// 必须检查才能用
if (x !== undefined) {
return x * 2;
}
return 0;
}
默认参数 =
function f(x: number = 0) {
// x: number(类型确定)
// 直接使用,无需检查
return x * 2;
}
建议:优先使用默认参数。函数体内部类型更确定,减少空值检查。
回调参数规则
回调函数可以忽略后面的参数
// 回调参数的特殊规则:可以传参数更少的回调
function fetchUser(callback: (user: string, age: number) => void) {
callback("张三", 25);
}
// 以下都是合法的
fetchUser((user, age) => console.log(user, age)); // 使用全部
fetchUser((user) => console.log(user)); // 忽略后面
fetchUser(() => console.log("完成")); // 忽略所有
// 这就是为什么 Array.forEach 的回调
// 可以只写 (item) 而 (item, index, array) 也合法
Part 3: 剩余参数
用 ... 收集剩余实参,类型必须是数组
有些函数的参数数量不固定,比如 sum(1, 2, 3)、sum(10, 20)。JS 用 ... 收集剩余参数,TS 则要求你给这些参数标注数组类型。这一节还会学一个高级技巧:用元组类型精确约束每个位置的类型,让 ...args 不再只是"一堆相同类型"。
剩余参数类型
用数组类型约束剩余参数
// 剩余参数类型是数组
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20); // 30
sum(); // 0(空数组)
// 与固定参数搭配
function log(level: string, ...messages: string[]): void {
console.log(`[${level}]`, messages.join(" "));
}
log("INFO", "服务器启动", "端口 3000");
// [INFO] 服务器启动 端口 3000
元组约束参数
用元组类型精确约束每个位置
// 用元组类型约束每个位置的参数
function createTuple(...pair: [string, number]): void {
console.log(pair[0], pair[1]);
// pair[0] 类型为 string
// pair[1] 类型为 number
}
createTuple("张三", 25); // OK
// createTuple("张三"); // 报错:缺少参数
// createTuple(25, "张三"); // 报错:类型不匹配
☕ 学习断点
函数类型的基础部分已完成,稍作休息
你已经掌握了参数类型、可选参数、剩余参数和泛型函数——这是日常开发最常用的函数类型知识。
建议:休息 5 分钟,可以尝试把工作中的 JS 函数加上类型注解练练手。
💡 后半部分将学习函数类型表达式、函数重载和 this 类型——这些是库作者和高级开发者需要掌握的内容。
Part 4: 泛型函数
让函数在不同类型下都能保持类型安全——泛型是 TS 最强大的工具之一
写一个函数,既能处理字符串数组,又能处理数字数组,还要保留类型信息——怎么做?用 any 类型全丢了,用联合类型又不够精确。泛型函数就是答案:把类型当作参数传进去,调用时自动推断。这一节从最简单的 identity 函数开始,逐步掌握泛型的推断、约束和多类型参数。
为什么需要泛型
用 any 会丢失类型信息,泛型帮你保留它
// 问题:identity 函数该怎样标注?
function identityAny(value: any): any {
return value;
}
const result = identityAny("hello");
// result 类型是 any —— 类型信息全丢了!
// 后续 result.toUpperCase() 不报错但可能运行时出错
// 解决:用泛型保留类型
function identity(value: T): T {
return value;
}
const r1 = identity("hello"); // r1 类型: string
const r2 = identity(42); // r2 类型: number
// 传入什么类型,返回什么类型,完整保留
泛型的核心思想:类型当作参数传入。调用时确定具体类型,编译时完整检查。
泛型推断机制
调用时 TS 如何自动推断类型参数?
// 推断规则:根据传入参数的类型,自动确定 T
function identity<T>(value: T): T {
return value;
}
// 从实参推断 T
identity("hello"); // T = string
identity(42); // T = number
identity([1, 2, 3]); // T = number[]
// 显式指定(推断不准或需要覆盖时)
identity<string[]>(["a"]); // T = string[]
// 推断失败的场景
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
// OK:两个参数都有,自动推断
merge({ name: "张三" }, { age: 25 });
// 需要显式:只传一个参数时
function createPair<T, U = number>(first: T, second: U): [T, U] {
return [first, second];
}
createPair("id", 1); // T=string, U=number
createPair<string>("id", 1); // 同上,U 使用默认值

图:泛型类型的推断与传播过程
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:展示泛型函数调用时类型参数 T 如何被推断和传播 - 左侧:函数定义 identity<T>(arg: T): T,T 用空心圆圈表示"未知类型" - 中间:调用处 identity("hello"),"hello" 的类型 string 用橙色箭头射入 T 的位置 - 右侧:T 被替换为 string 后的函数签名 identity(arg: string): string,全部用实心橙色填充 - 底部标注:"定义时 T 是占位符 → 调用时 T 被推断 → T 传播到所有使用处" - 配色建议:浅灰背景,未知类型用虚线蓝色框,推断后用实心橙色,箭头用深灰 - 画面比例:16:9 - 避免:过度装饰、3D 效果
泛型基本语法
类型参数 <T> 写在函数名后面
// 基本语法: 是类型参数,调用时自动推断
function identity(value: T): T {
return value;
}
// 方式1:让 TS 自动推断(推荐)
identity("hello"); // T 推断为 string
identity(42); // T 推断为 number
// 方式2:显式指定类型参数
identity("hello"); // 显式指定 T = string
identity([1, 2]); // 显式指定 T = number[]
// 泛型也用于工具函数
function first(arr: T[]): T | undefined {
return arr[0];
}
first(["a", "b"]); // 返回类型: string | undefined
first([1, 2, 3]); // 返回类型: number | undefined
first([]); // 返回类型: undefined
泛型约束
用 extends 限定类型参数的范围
// 约束:T 必须有 length 属性
function logLength(value: T): void {
console.log(value.length);
}
logLength("hello"); // OK,string 有 length
logLength([1, 2, 3]); // OK,数组有 length
logLength({ length: 10 }); // OK,对象有 length
// logLength(123); // 报错!number 没有 length
// 多类型参数:常见于 map、merge 等函数
function map(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const nums = [1, 2, 3];
const strs = map(nums, n => n.toString());
// T = number, U = string → 返回 string[]
function merge(a: A, b: B): A & B {
return { ...a, ...b };
}
const merged = merge({ name: "张三" }, { age: 25 });
// 类型: { name: string } & { age: number }
多类型参数实战
泛型约束 + 多类型参数的常见组合
// keyof 约束:安全地访问对象属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25, active: true };
getProperty(user, "name"); // 返回 string
getProperty(user, "age"); // 返回 number
// getProperty(user, "email"); // 报错!"email" 不是 keyof user
// 条件约束:T 必须是 U 的子类型
function assign<T extends U, U>(target: T, source: U): T {
return Object.assign(target, source);
}
// 泛型默认值
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
createArray(3, "x"); // string[]
createArray(3, 0); // number[]
createArray<number>(3, 0); // number[](显式覆盖默认值)
约束链:extends keyof 是泛型约束中最强大的组合之一,确保类型安全的属性访问,是很多工具类型(如 Pick、Omit)的底层原理。
泛型箭头函数
注意 TSX 文件中的写法陷阱
// 泛型箭头函数
const identity = (value: T): T => value;
// ^^^ 注意这个逗号!
// 为什么加逗号?在 .tsx 文件中 会被当作 JSX 标签
// 加逗号 告诉 TS 这是泛型,不是 JSX
const first = (arr: T[]): T | undefined => arr[0];
// 常用泛型工具函数
const last = (arr: T[]): T | undefined => arr[arr.length - 1];
const pluck = (obj: T, key: K): T[K] => obj[key];
const person = { name: "张三", age: 25 };
pluck(person, "name"); // 返回类型: string
pluck(person, "age"); // 返回类型: number
泛型工具模式
first、last、pluck——泛型工具函数三剑客
// first:安全取数组首元素
const first = <T,>(arr: T[]): T | undefined => arr[0];
first([1, 2, 3]); // number | undefined → 1
first([]); // undefined
// last:安全取数组末元素
const last = <T,>(arr: T[]): T | undefined => arr[arr.length - 1];
last(["a", "b"]); // string | undefined → "b"
// pluck:从对象数组中提取指定字段
const pluck = <T, K extends keyof T>(arr: T[], key: K): T[K][] =>
arr.map(item => item[key]);
const users = [
{ name: "张三", age: 25 },
{ name: "李四", age: 30 },
];
pluck(users, "name"); // string[] → ["张三", "李四"]
pluck(users, "age"); // number[] → [25, 30]
// notEmpty:过滤空值,收窄类型
function notEmpty<T>(value: T | null | undefined): value is T {
return value != null;
}
const items: (string | null)[] = ["a", null, "b", null, "c"];
const result = items.filter(notEmpty);
// result 类型: string[](不是 (string | null)[])
这些工具函数的共同特征:输入类型决定输出类型,泛型让它们对任何类型都安全。掌握这几个模式,日常开发中 80% 的泛型场景都能应对。
Part 5: 函数类型表达式
函数不仅是值,也是一种类型。可以用类型别名来描述函数签名
前面一直在给函数的参数和返回值标注类型,但函数本身也是一种类型。能不能把"接收两个 number、返回 number 的函数"抽象成一个类型别名?可以。这一节学习用 type 和 interface 描述函数签名,以及高阶函数、柯里化这些函数式编程场景中的类型写法。
类型别名
用 type 描述函数的签名
// 使用 type 定义函数类型
type MathFn = (a: number, b: number) => number;
// 赋值符合签名的函数
const add: MathFn = (a, b) => a + b;
const multiply: MathFn = (a, b) => a * b;
add(3, 4); // 7
multiply(3, 4); // 12
// TS 会根据 MathFn 自动推断 a、b 的类型,无需重复标注
接口中的函数
三种写法,推荐按场景选择
interface Calculator {
// 写法1:方法语法
add(a: number, b: number): number;
// 写法2:属性语法
subtract: (a: number, b: number) => number;
// 写法3:简写
multiply(a: number, b: number): number;
}
const calc: Calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
建议:函数会被重新赋值或作为回调时,用属性语法 fn: () => void;函数是对象固有方法时,用方法语法 fn(): void。
构造签名
描述可以被 new 调用的函数
// 描述可以被 new 调用的函数(类/构造函数)
type Constructor = new (name: string) => { name: string };
class Person {
constructor(public name: string) {}
}
function factory(Ctor: Constructor, name: string) {
return new Ctor(name);
}
const p = factory(Person, "张三");
console.log(p.name); // "张三"
高阶函数类型
接收函数作为参数、或返回函数的函数
// 高阶函数:接收函数,返回函数
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map();
return ((...args: any[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
const expensiveCalc = memoize((n: number): number => {
console.log("计算中...");
return n * n;
});
expensiveCalc(5); // 计算中... → 25
expensiveCalc(5); // 直接返回 25(缓存命中)
// 柯里化函数类型
function curry<T, U, V>(fn: (a: T, b: U) => V): (a: T) => (b: U) => V {
return (a) => (b) => fn(a, b);
}
const add = (a: number, b: number): number => a + b;
const add5 = curry(add)(5); // (b: number) => number
add5(3); // 8
add5(10); // 15
类型推导链:高阶函数的威力在于——泛型参数从最内层的函数一路传递到最外层,每一步都有完整的类型推断。这就是函数式编程在 TS 中如此流畅的原因。
函数组合类型
pipe / compose 的类型安全实现
// pipe:从左到右依次执行函数
function pipe<A, B>(f1: (a: A) => B): (a: A) => B;
function pipe<A, B, C>(f1: (a: A) => B, f2: (b: B) => C): (a: A) => C;
function pipe<A, B, C, D>(
f1: (a: A) => B,
f2: (b: B) => C,
f3: (c: C) => D
): (a: A) => D;
function pipe(...fns: Function[]): Function {
return (input: any) => fns.reduce((acc, fn) => fn(acc), input);
}
// 类型安全:每一步的输出类型是下一步的输入类型
const transform = pipe(
(s: string) => s.trim(),
(s: string) => s.toUpperCase(),
(s: string) => s.split(""),
(arr: string[]) => arr.length
);
transform(" hello "); // 5
ts-toolbelt 或 remeda 等库提供的已类型化版本。
Part 6: 函数重载
同一个函数名对应多种参数组合,每种有不同的返回类型
有些函数根据传入参数的类型不同,返回类型也不同。比如传字符串返回数组,传数组返回字符串。联合类型搞不定这种"输入决定输出"的场景——这时候就需要函数重载。这一节学习重载的写法、排序规则,以及什么时候该用重载、什么时候该用联合类型替代。
基本重载
不同参数类型,不同返回类型
// 重载签名:定义函数的不同调用方式
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
// 实现签名:必须兼容所有重载签名
function format(value: string | number | boolean): string {
if (typeof value === "string") return value.trim();
if (typeof value === "number") return value.toFixed(2);
return value ? "是" : "否";
}
format(" hello "); // "hello" —— TS 知道返回 string
format(3.14159); // "3.14"
format(true); // "是"
重载注意事项
能用联合类型解决的,不要用重载
// 重载版本(不推荐):
function padLeft(value: string, padding: string): string;
function padLeft(value: string, padding: number): string;
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
// 联合类型版本(推荐):
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
重载排序规则
最具体的签名放最前面,实现签名不可直接调用
// 重载排序:从最具体到最宽泛
function getItems(count: 1): string; // 最具体
function getItems(count: number): string[]; // 较宽泛
function getItems(count: number): string | string[] {
if (count === 1) return "only one";
return Array(count).fill("item");
}
getItems(1); // 返回 string(匹配第一个签名)
getItems(3); // 返回 string[](匹配第二个签名)

图:函数重载的匹配流程
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个从上到下的决策流程图,展示 TypeScript 如何选择重载签名 - 顶部:调用表达式 parseJSON("42"),箭头向下 - 中间:三个重载签名从上到下排列,第一个匹配成功(参数为 string)用绿色勾号标注 - 下方:实现签名用虚线框标注"对调用者不可见" - 右侧注释框:"重载签名从上到下匹配 → 第一个匹配的胜出 → 实现签名不参与匹配" - 配色建议:浅蓝背景,匹配成功用绿色,未匹配用灰色,实现签名用虚线边框 - 画面比例:16:9 - 避免:过度装饰、3D 效果
// 常见错误:重载签名参数数量相同,TS 无法区分
function fn(x: string): void;
function fn(x: number): void;
// 以上两个参数数量相同,TS 按顺序匹配,可能不符合预期
// 建议:加额外参数区分,或改用联合类型
重载 vs 联合类型
什么时候用重载,什么时候用联合类型?
| 场景 | 推荐方案 |
|---|---|
| 参数不同类型 → 返回相同类型 | 联合类型参数 |
| 参数不同类型 → 返回不同类型 | 函数重载 |
| 参数数量不同 | 可选参数 / 默认参数 |
| 需要根据参数字面量精确返回类型 | 函数重载 |
// 适合重载:返回类型随参数变化
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement;
// 适合联合类型:返回类型不变
function format(value: string | number): string {
return typeof value === "number" ? value.toFixed(2) : value;
}
经验法则:先试联合类型。如果联合类型能表达你的意图,就不要用重载。重载是联合类型搞不定时的升级武器。
接口中的重载
对象类型和类方法也可以重载
// 接口中的重载
interface StringStore {
get(key: string): string | undefined;
get(key: string, defaultValue: string): string;
set(key: string, value: string): void;
}
const store: StringStore = {
get(key: string, defaultValue?: string): string | undefined {
// 实现
return defaultValue ?? undefined;
},
set(key: string, value: string): void {
// 实现
}
};
// 类方法重载
class Formatter {
format(value: string): string;
format(value: number): string;
format(value: string | number): string {
return typeof value === "number"
? value.toFixed(2)
: value.trim();
}
}
const fmt = new Formatter();
fmt.format(" hello "); // "hello"
fmt.format(3.14); // "3.14"
重载胜出场景
联合类型搞不定、只有重载能解决的 3 个场景
// 场景1:字面量类型 → 精确返回类型
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const input = createElement("input"); // HTMLInputElement
// 联合类型只能返回 HTMLInputElement | HTMLCanvasElement | HTMLElement
// 场景2:参数数量不同 → 不同返回类型
function query(selector: string): Element | null;
function query<T extends HTMLElement>(sel: string, all: true): T[];
function query(sel: string, all?: boolean): Element | null | Element[] {
return all
? [...document.querySelectorAll(sel)]
: document.querySelector(sel);
}
query(".box"); // Element | null
query<HTMLDivElement>(".box", true); // HTMLDivElement[]
// 场景3:互斥参数组合(有 A 就不能有 B)
function configure(options: { url: string; method?: string }): void;
function configure(options: { file: File }): void;
function configure(options: any): void { /* ... */ }
configure({ url: "/api" }); // OK
configure({ file: new File([], "a.txt") }); // OK
记忆口诀:字面量精确返回、参数数量/结构不同、互斥参数——这三类场景用重载,其余用联合类型。
Part 7: this 与类型谓词
this 参数标注和自定义类型守卫,是进阶函数类型技巧
函数类型的基础和进阶都学了,最后两个"大招"留给这一节。this 参数标注让你在方法中安全地使用 this,不再担心回调丢失绑定。类型谓词(val is T)让你自定义类型守卫,在条件分支中精确收窄类型。这两个技巧在 React、Express 等框架开发中会反复用到。
this 参数标注
告诉 TS 函数里的 this 是什么类型
// 函数的第一个参数可以声明 this 的类型
interface DB {
filter(query: string): DB;
limit(n: number): DB[];
}
function query(this: DB, sql: string): DB[] {
// this 的类型被标注为 DB
return this.filter(sql).limit(10);
}
// this 参数是编译时假的参数,运行时不存在
// 箭头函数没有自己的 this
class Timer {
private seconds = 0;
start() {
// 箭头函数自动绑定外层 this
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
事件回调与this
DOM 事件监听器中 this 的指向问题
// 问题:回调函数中的 this 丢失
class Button {
constructor(private label: string) {}
// 方法作为回调时 this 丢失
handleClick() {
console.log(this.label); // this 可能是 undefined!
}
}
const btn = new Button("提交");
// document.addEventListener("click", btn.handleClick); // 运行时出错!
// handleClick 里的 this 不再指向 btn 实例
// 解决方案1:箭头函数包装
document.addEventListener("click", () => btn.handleClick());
// 解决方案2:bind 绑定 this
document.addEventListener("click", btn.handleClick.bind(btn));
// 解决方案3:在构造函数中绑定
class Button2 {
constructor(private label: string) {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.label); // this 现在安全
}
}
this 会在调用时重新确定。箭头函数没有自己的 this,始终使用定义时的 this。
箭头函数属性
类属性箭头函数 = 自动绑定 this 的方法
// 类属性箭头函数:自动绑定 this
class Counter {
count = 0;
// 箭头函数作为类属性 → 自动绑定实例
increment = () => {
this.count++;
console.log(this.count);
};
// 普通方法 → 不自动绑定
decrement() {
this.count--;
}
}
const counter = new Counter();
const inc = counter.increment;
inc(); // 1 — 箭头函数,this 正确
const dec = counter.decrement;
// dec(); // 运行时报错!this 不指向 counter 实例
// this 参数 + strictBindCallApply
class Logger {
constructor(private prefix: string) {}
// 用 this 参数声明 this 的期望类型
log(this: Logger, message: string): void {
console.log(`[${this.prefix}] ${message}`);
}
}
const logger = new Logger("APP");
const logFn = logger.log;
// logFn("hello"); // TS 报错:this 不匹配 Logger 类型
// 解决:bind 明确绑定
const boundLog = logger.log.bind(logger);
boundLog("hello"); // [APP] hello
权衡:箭头函数属性每次实例化都创建新函数,占用稍多内存。bind 在构造函数中只绑定一次。对于大多数场景,选择可读性更高的方案即可。
bind/call/apply
TypeScript 中这三个方法的类型签名
// bind:创建新函数,永久绑定 this 和部分参数
function greet(this: { name: string }, greeting: string): string {
return `${greeting}, ${this.name}!`;
}
const boundGreet = greet.bind({ name: "张三" });
boundGreet("你好"); // "你好, 张三!"
// 预设参数(柯里化)
function add(a: number, b: number): number {
return a + b;
}
const add5 = add.bind(null, 5);
add5(3); // 8
// call 和 apply:立即调用,临时改变 this
function greet2(this: { name: string }, greeting: string): string {
return `${greeting}, ${this.name}!`;
}
greet2.call({ name: "李四" }, "早上好"); // "早上好, 李四!"
greet2.apply({ name: "王五" }, ["晚安"]); // "晚安, 王五!"
// call vs apply 的区别:
// call(thisArg, arg1, arg2, ...) —— 逐个传参
// apply(thisArg, [arg1, arg2]) —— 数组传参
// strictBindCallApply 开启后的类型安全
// TS 会根据函数签名自动推断 bind/call/apply 的参数类型
function format(template: string, ...args: string[]): string {
return template.replace(/\{(\d+)\}/g, (_, i) => args[i]);
}
format.bind(null, "Hello {0}"); // 预设模板
// format.call(null, 123); // 报错!第一个参数必须是 string
// format.apply(null, ["hi", 123]); // 报错!args 必须是 string[]
tsconfig.json 中开启 "strict": true(包含 strictBindCallApply),否则 bind/call/apply 的参数类型不会被检查。
类型谓词
自定义守卫,让 TS 在条件分支中收窄类型
// 类型谓词让 TS 在条件分支中收窄类型
function isString(val: unknown): val is string {
return typeof val === "string";
}
function process(value: string | number) {
if (isString(value)) {
// 这里 value 被 TS 收窄为 string
console.log(value.toUpperCase());
} else {
// 这里 value 被 TS 收窄为 number
console.log(value.toFixed(2));
}
}
// 实际应用:判断 API 返回数据
interface SuccessResponse { status: "ok"; data: string[]; }
interface ErrorResponse { status: "error"; message: string; }
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(res: ApiResponse): res is SuccessResponse {
return res.status === "ok";
}
断言函数
调用后直接收窄类型,不返回的异常由 TS 跟踪
// 断言函数(asserts)——如果函数正常返回,则条件一定成立
function assertDefined<T>(
val: T | null | undefined,
msg?: string
): asserts val is T {
if (val == null) {
throw new Error(msg ?? "值不能为 null 或 undefined");
}
}
const input: string | null = getInputValue();
assertDefined(input, "输入不能为空");
console.log(input.length); // OK,TS 知道 input 是 string
类型谓词用在 if 中收窄,断言函数调用后直接收窄。两者互补,按场景选择。
异步函数类型
async 函数的返回类型怎么标?
// async 函数总是返回 Promise
// 写法1:显式标注 Promise<T>
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// 写法2:让 TS 自动推断(推荐)
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json(); // 自动推断为 Promise<User>
}
// 注意:不要重复包装 Promise
async function bad(): Promise<Promise<string>> {
return Promise.resolve("hello"); // 多包了一层!
}
// 普通函数返回 Promise,不加 async 也可以
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
记住:标注 async 函数返回类型时,用 Promise<T> 而不是 T。不加 async 但返回 Promise 的函数,返回类型也得标 Promise<T>。
重载实战场景
DOM API 和工具函数里的重载长什么样?
// DOM: createElement 的重载签名(简化版)
declare function createElement(tag: "div"): HTMLDivElement;
declare function createElement(tag: "span"): HTMLSpanElement;
declare function createElement(tag: "canvas"): HTMLCanvasElement;
declare function createElement(tag: string): HTMLElement;
// 调用时返回类型自动精确
const div = createElement("div"); // HTMLDivElement
const span = createElement("span"); // HTMLSpanElement
// 工具函数:根据输入类型决定返回类型
function parseInput(input: string): string[];
function parseInput(input: string[]): string;
function parseInput(input: string | string[]): string | string[] {
if (typeof input === "string") {
return input.split(",");
}
return input.join(",");
}
parseInput("a,b,c"); // string[] → ["a","b","c"]
parseInput(["a","b"]); // string → "a,b"
类型收窄流程
联合类型参数如何一步步缩小到精确类型?

图:函数参数类型收窄流程
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:从左到右的收窄流程 - 起始节点:接收联合类型参数 string | number | string[] - 中间:typeof/instanceof/in 类型判断,用菱形决策节点表示 - 三条收窄路径: - typeof === "string" → 收窄为 string → 调用 toUpperCase() - typeof === "number" → 收窄为 number → 调用 toFixed() - Array.isArray() → 收窄为 string[] → 调用 .join() - 每条路径用不同颜色标注 - 配色建议:浅蓝背景,判断菱形橙色,收窄结果绿色卡片,箭头深灰 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
function process(value: string | number | string[]) {
if (typeof value === "string") {
// 收窄为 string → 可用 toUpperCase
return value.toUpperCase();
}
if (typeof value === "number") {
// 收窄为 number → 可用 toFixed
return value.toFixed(2);
}
// 收窄为 string[] → 可用 join
return value.join(", ");
}
练习:重载 parseJSON
用函数重载实现一个类型安全的 JSON 解析器
需求:输入 string 返回解析后的对象;输入 Buffer 也返回解析后的对象。
// 先定义重载签名
function parseJSON(input: string): Record<string, unknown>;
function parseJSON(input: Buffer): Record<string, unknown>;
// 再实现
function parseJSON(input: string | Buffer): Record<string, unknown> {
// 你的实现
}
参考答案
function parseJSON(input: string): Record<string, unknown>;
function parseJSON(input: Buffer): Record<string, unknown>;
function parseJSON(input: string | Buffer): Record<string, unknown> {
const str = typeof input === "string"
? input
: input.toString("utf-8");
return JSON.parse(str);
}
// 使用
const obj1 = parseJSON('{"name":"张三"}'); // string → 对象
const buf = Buffer.from('{"age":25}');
const obj2 = parseJSON(buf); // Buffer → 对象
常见函数类型错误
问题一:不能将类型"void"分配给类型"number"
原因:函数没有 return 语句,但标注了返回 number。
修复:检查函数体是否有 return,或者把返回类型改成 void。
问题二:应有 2 个参数,但获得 1 个
原因:调用时漏传了必选参数。
修复:补上参数,或者把参数改为可选(加 ?)或设默认值。
问题三:重载签名不兼容实现签名
原因:实现签名的参数类型范围太窄。
修复:实现签名的参数类型必须包含所有重载签名的类型。
试一试
练习一
任务:给下面的函数添加完整的类型注解。
function createUser(name, age, role) {
return { name, age, role };
}
参考答案
interface User {
name: string;
age: number;
role: string;
}
function createUser(
name: string,
age: number,
role: string = "user"
): User {
return { name, age, role };
}
练习二
任务:写一个类型谓词函数 isNonNull,过滤掉数组中的 null。
const items: (string | null)[] = ["a", null, "b", null, "c"];
// 用 filter + isNonNull 得到 string[]
参考答案
function isNonNull<T>(val: T | null): val is T {
return val !== null;
}
const items: (string | null)[] = ["a", null, "b", null, "c"];
const filtered = items.filter(isNonNull);
// filtered 类型: string[]
📝 要点总结
本章核心知识点
参数与返回
必选、可选、默认、剩余参数的类型标注方式
泛型函数
类型参数 T、约束 extends、默认类型参数
函数重载
多签名声明,按顺序匹配,实现不同类型输入输出
this 与回调
this 参数标注、箭头函数属性、回调参数规则
类型收窄
类型谓词 value is T、断言函数 asserts condition
下一章
函数类型掌握了,下一章学习对象与接口——用 interface 描述对象的形状。