前置回顾
- 第 02 篇讲了
const/let 声明变量, 以及 number, string, boolean 等基础类型
- 类型注解写在变量名后面, 大多数情况编译器能自动推断
- 本篇进入函数: TS 中最常手写类型标注的地方
一句话总结
函数参数必须标注类型(不会推断), 返回值通常可以省略(会推断). TS 支持箭头函数, 可选参数, 默认值, 剩余参数, 重载, 以及将函数类型作为值传递.
函数声明的几种方式
TS 有三种写法定义函数, 最终编译结果几乎等价:
function add(a: number, b: number): number { return a + b; }
const add2 = (a: number, b: number): number => { return a + b; };
const add3 = (a: number, b: number): number => a + b;
|
对比其他语言:
func add(a int, b int) int { return a + b }
var add2 = func(a, b int) int { return a + b }
|
fn add(a: i32, b: i32) -> i32 { a + b } let add2 = |a: i32, b: i32| -> i32 { a + b };
|
何时用哪种
| 场景 |
推荐写法 |
| 模块顶层的独立函数 |
function 声明 |
| 回调, 传参, 短逻辑 |
箭头函数 |
需要 this 绑定外层上下文 |
箭头函数 |
| 需要函数提升(调用写在声明前) |
function 声明 |
function 声明会提升
function 声明有提升(hoisting), 可以先调用再定义. 箭头函数赋值给 const 则不行:
greet("Alice");
function greet(name: string) { console.log(name); }
|
greet("Alice");
const greet = (name: string) => { console.log(name); };
|
参数类型标注
函数参数不会被推断, 必须手动标注. 这是 TS 中最常写类型的位置:
function repeat(str: string, times: number): string { return str.repeat(times); }
|
可选参数
参数名后加 ? 表示可以不传, 类型自动变为 T | undefined:
function greet(name: string, greeting?: string): string { return `${greeting || "Hello"}, ${name}`; }
greet("Alice"); greet("Alice", "Hi");
|
Go 没有可选参数, 通常用 Options 结构体或函数式选项模式替代. TS 的 ? 更直接.
可选参数必须在必选参数后面
function bad(a?: string, b: number) {} function ok(b: number, a?: string) {}
|
默认参数
给参数赋默认值, 不传时使用默认值. 有默认值的参数不需要 ?:
function createServer(port: number = 3000, host: string = "localhost") { console.log(`${host}:${port}`); }
createServer(); createServer(8080); createServer(8080, "0.0.0.0");
|
编译器从默认值推断参数类型, 所以 port: number = 3000 中的 : number 可以省略为 port = 3000.
剩余参数
用 ... 收集不定数量的参数为数组:
function sum(...numbers: number[]): number { return numbers.reduce((acc, n) => acc + n, 0); }
sum(1, 2, 3); sum(1, 2, 3, 4);
|
对比 Go 的变参:
func sum(numbers ...int) int { total := 0 for _, n := range numbers { total += n } return total }
|
语法几乎对称: Go 的 ...int 放在类型前, TS 的 ...numbers: number[] 放在参数名前.
返回值类型
显式标注 vs 自动推断
function add(a: number, b: number): number { return a + b; }
function add2(a: number, b: number) { return a + b; }
|
什么时候该显式写
三种情况建议显式标注返回类型:
- 公共 API / 导出函数: 返回类型是契约, 防止实现意外改变时影响调用方
- 递归函数: 编译器有时推断不出递归的返回类型
- 复杂逻辑: 多个 return 路径, 显式标注能帮助发现遗漏
export function parseConfig(raw: string): AppConfig { return JSON.parse(raw); }
const double = (n: number) => n * 2;
|
void: 无返回值
函数不返回有意义的值时, 返回类型为 void:
function log(msg: string): void { console.log(msg); }
|
void 表示调用方不应使用返回值. 实际运行时返回 undefined, 但类型层面禁止对它做任何操作.
多返回值
Go 的多返回值在 TS 中不存在. 替代方案:
function divide(a: number, b: number): [number, boolean] { if (b === 0) return [0, false]; return [a / b, true]; } const [result, ok] = divide(10, 3);
function divide2(a: number, b: number): { result: number; ok: boolean } { if (b === 0) return { result: 0, ok: false }; return { result: a / b, ok: true }; } const { result: r, ok } = divide2(10, 3);
|
对比 Go:
func divide(a, b float64) (float64, bool) { if b == 0 { return 0, false } return a / b, true } result, ok := divide(10, 3)
|
推荐: 返回对象而非元组
超过 2 个返回值时用对象. 元组的 [0], [1] 可读性差, 对象字段名自解释.
Go 靠命名返回值(result float64, ok bool)解决可读性, TS 靠对象.
函数重载
同一个函数名, 根据参数不同做不同的事. TS 用重载签名实现:
function format(value: string): string; function format(value: number): string;
function format(value: string | number): string { if (typeof value === "string") { return value.trim(); } return value.toFixed(2); }
format("hello "); format(3.14159); format(true);
|
对比其他语言:
| 语言 |
重载方式 |
| Java |
同名方法, 参数列表不同 |
| Go |
不支持重载, 用不同函数名 |
| Rust |
不支持重载, 用 trait / enum 分发 |
| TypeScript |
重载签名 + 单一实现 |
重载不是多个函数体
TS 的重载和 Java 不同: 只有一个实现, 内部靠类型判断分支. 重载签名只用于调用方的类型检查, 不生成多份代码.
多数情况下, 联合类型 + 类型收窄比重载更简洁. 只在"参数类型和返回类型有对应关系"时才用重载.
函数类型表达式
函数本身也是值, 它的类型可以这样描述:
type MathFn = (a: number, b: number) => number;
const add: MathFn = (a, b) => a + b; const multiply: MathFn = (a, b) => a * b;
|
对比 Go:
type MathFn func(int, int) int
var add MathFn = func(a, b int) int { return a + b }
|
函数类型最常用于回调参数:
function applyOperation(a: number, b: number, fn: (x: number, y: number) => number): number { return fn(a, b); }
applyOperation(3, 4, (x, y) => x + y);
|
用 type 抽出来可读性更好:
type BinaryOp = (x: number, y: number) => number;
function applyOperation(a: number, b: number, fn: BinaryOp): number { return fn(a, b); }
|
this 的问题
Go 和 Rust 中, 方法的接收者是显式的(self / *MyStruct). JS/TS 的 this 取决于调用方式, 不取决于定义位置:
const user = { name: "Alice", greet() { console.log(this.name); }, };
user.greet();
const fn = user.greet; fn();
|
Go/Rust 的 self = 写在函数签名里的参数, 调用时编译器自动填.
JS 的 this = 运行时根据"谁调了这个函数"动态决定, 像一张临时工牌, 随调用方式变.
箭头函数固定 this
箭头函数不创建自己的 this, 而是捕获定义时外层的 this:
const user = { name: "Alice", greet() { const inner = () => console.log(this.name); inner(); }, };
|
这是箭头函数在回调场景中被优先选择的核心原因.
为 this 标注类型
TS 允许把 this 作为第一个伪参数标注类型(不影响调用):
interface Button { label: string; onClick(this: Button): void; }
const btn: Button = { label: "Submit", onClick() { console.log(this.label); }, };
|
实际使用中
大多数时候不需要手动标注 this. 用箭头函数或 class 方法可以避免 this 问题.
只在定义回调接口或事件处理器签名时偶尔用到.
泛型函数入门
有时函数的输入和输出类型相关, 但具体是什么类型不确定. 用泛型参数 <T> 表示:
function identity<T>(arg: T): T { return arg; }
identity<string>("hello"); identity(42);
|
三语言对比:
function first<T>(arr: T[]): T | undefined { return arr[0]; }
|
func First[T any](arr []T) (T, bool) { if len(arr) == 0 { var zero T; return zero, false } return arr[0], true }
|
fn first<T>(arr: &[T]) -> Option<&T> { arr.first() }
|
泛型的详细用法(约束, 多类型参数, 工具类型)在阶段三专门讲. 这里只需记住: <T> 是类型的"参数", 让函数适配多种类型而不丢失类型信息.
泛型和 any 的区别
any 放弃了类型信息: 传入 string, 返回也是 any, 调用方拿到 any 后续操作全无检查.
泛型保留类型关联: 传入 string, 返回就是 string. 类型安全没有丢失.
回调与高阶函数
函数可以作为参数传递, 也可以作为返回值. 这在 TS 中极为常见:
const numbers = [1, 2, 3, 4]; const doubled = numbers.map((n) => n * 2); const strings = numbers.map((n) => String(n));
const evens = numbers.filter((n) => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
|
对比 Go(需要自己写循环或用泛型工具库):
doubled := slices.Collect(func(yield func(int) bool) { for _, n := range numbers { yield(n * 2) } })
|
对比 Rust:
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
|
自定义高阶函数
返回函数的函数:
function createMultiplier(factor: number): (n: number) => number { return (n) => n * factor; }
const double = createMultiplier(2); const triple = createMultiplier(3);
double(5); triple(5);
|
这就是闭包: 内部函数捕获外部变量 factor. 和 Go/Rust 的闭包概念完全一致, 只是语法不同.
快速回顾
- 三种声明:
function 具名声明(有提升), 箭头函数(绑定外层 this), 函数表达式
- 参数类型: 必须手动标注, 支持可选(
?), 默认值(= x), 剩余参数(...args)
- 返回值: 通常靠推断, 导出函数和递归建议显式标注
- 多返回值: TS 没有, 用对象或元组替代
- 函数重载: 多个签名 + 单一实现, 只在参数/返回类型有对应关系时使用
- 函数类型:
type Fn = (a: T) => R, 用于回调参数和变量声明
- this: 动态绑定, 箭头函数固定为外层 this
- 泛型:
<T> 是类型参数, 保留类型关联而不丢失为 any
动手练习
- 基础函数: 写一个
clamp(value, min, max) 函数, 将 value 限制在 [min, max] 范围内, 标注完整类型.
- 可选参数: 写一个
formatName(first, last?, title?) 函数, 根据传入参数数量组合出不同格式的名字.
- 回调参数: 写一个
retry(fn, times) 函数, 接收一个可能失败的回调, 失败时重试 times 次.
- 函数类型: 定义一个
type Comparator<T> = (a: T, b: T) => number, 然后写一个通用排序函数使用它.
- 泛型入门: 写一个
function last<T>(arr: T[]): T | undefined 返回数组最后一个元素.
- 重载练习: 写一个
stringify 函数, 接收 number 返回固定小数位字符串, 接收 Date 返回 ISO 格式字符串, 用重载签名实现.