← 返回目录

第 2.2 章:函数与接口

TypeScript 让函数签名变成可靠的契约

⚡ 函数类型注解

TypeScript 允许你为函数的参数和返回值添加类型注解,使函数签名成为一份明确的契约。

普通函数写法:

function add(a: number, b: number): number {
    return a + b;
}

function greet(name: string): string {
    return `你好,${name}!`;
}

// 返回 void 表示没有返回值
function log(message: string): void {
    console.log(message);
}

等价的箭头函数写法:

const add = (a: number, b: number): number => a + b;

const greet = (name: string): string => `你好,${name}!`;

// 多行箭头函数
const processData = (input: string): string[] => {
    const parts = input.split(",");
    return parts.map(p => p.trim());
};

💡 类型推断:

如果函数体很简单,TypeScript 能自动推断返回类型。但对于公共 API 和库函数,显式标注返回类型是最佳实践——它既是文档,也防止意外修改返回值类型。

❓ 可选参数与默认值

? 标记可选参数,用 = 设置默认值,用 ...args 收集剩余参数。

// 可选参数:middleName 可以不传
function fullName(first: string, last: string, middleName?: string): string {
    return middleName ? `${first} ${middleName} ${last}` : `${first} ${last}`;
}

fullName("张", "三");           // "张 三"
fullName("张", "三", "小明");   // "张 小明 三"

// 默认值:有默认值的参数自动变为可选
function createUrl(path: string, base: string = "https://api.example.com"): string {
    return `${base}${path}`;
}

createUrl("/users");                     // "https://api.example.com/users"
createUrl("/users", "http://localhost");  // "http://localhost/users"

// 剩余参数:收集任意数量的参数
function sum(...numbers: number[]): number {
    return numbers.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3, 4, 5);  // 15

⚠️ 注意:

可选参数必须放在必选参数之后。如果你需要中间的参数可选,考虑使用对象参数或函数重载。

🔀 函数重载

当一个函数根据不同的输入类型返回不同的类型时,可以使用重载签名来精确描述这种行为。

// 重载签名(仅声明,不实现)
function parse(input: string): number;
function parse(input: number): string;
// 实现签名(必须兼容所有重载)
function parse(input: string | number): number | string {
    if (typeof input === "string") {
        return parseInt(input, 10);
    }
    return input.toString();
}

const num = parse("42");    // 类型推断为 number
const str = parse(42);      // 类型推断为 string

更实用的例子——根据参数数量重载:

function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content?: string): HTMLElement {
    const el = document.createElement(tag);
    if (content) el.textContent = content;
    return el;
}

💡 重载 vs 联合类型:

如果输入/输出类型之间没有对应关系,直接用联合类型更简洁。只有当"输入 A → 返回 X,输入 B → 返回 Y"这种映射关系需要精确表达时,才使用重载。

📐 interface 接口

接口用于描述对象的形状(shape),是 TypeScript 最核心的类型构建工具之一。

interface User {
    id: number;
    name: string;
    email: string;
    age?: number;               // 可选属性
    readonly createdAt: Date;   // 只读属性
}

const user: User = {
    id: 1,
    name: "张三",
    email: "zhang@example.com",
    createdAt: new Date(),
};

// user.createdAt = new Date(); // ❌ 编译错误:readonly 不可修改

索引签名——当你不确定属性名但知道值类型时:

interface StringMap {
    [key: string]: string;
}

const headers: StringMap = {
    "Content-Type": "application/json",
    "Authorization": "Bearer token123",
};

interface NumberArray {
    [index: number]: string;
    length: number;
}

🔄 与 Java 的区别:

Java 用 interface 定义行为契约,类需要 implements 显式声明实现。TypeScript 的 interface 是纯结构检查(duck typing),对象只要结构匹配就自动兼容——不需要任何显式声明。这意味着你可以为第三方库的对象定义接口,而无需修改其源代码。

🧬 接口继承与组合

接口可以通过 extends 继承其他接口,支持多继承。

interface HasId {
    id: number;
}

interface HasTimestamps {
    createdAt: Date;
    updatedAt: Date;
}

// 继承多个接口
interface User extends HasId, HasTimestamps {
    name: string;
    email: string;
}

// 等价于手写所有属性:
// interface User {
//     id: number;
//     createdAt: Date;
//     updatedAt: Date;
//     name: string;
//     email: string;
// }

const user: User = {
    id: 1,
    name: "李四",
    email: "li@example.com",
    createdAt: new Date(),
    updatedAt: new Date(),
};

💡 声明合并:

同名的 interface 会自动合并属性(declaration merging)。这在扩展第三方库的类型定义时非常有用,但也容易造成意外——注意命名冲突。

🏷️ type 别名

type 关键字可以为任何类型创建别名,比 interface 更灵活。

// 基本别名
type ID = string | number;
type Status = "active" | "inactive" | "banned";

// 对象类型(与 interface 类似)
type Point = {
    x: number;
    y: number;
};

// 联合类型
type Result<T> = { success: true; data: T } | { success: false; error: string };

// 交叉类型(合并多个类型)
type WithId = { id: number };
type WithName = { name: string };
type Entity = WithId & WithName;  // { id: number; name: string }

// 使用内置工具类型
type PartialUser = Partial<User>;       // 所有属性变为可选
type ReadonlyUser = Readonly<User>;     // 所有属性变为只读
type UserName = Pick<User, "name" | "email">;  // 只取部分属性

🤔 type vs interface:如何选择?

  • interface:描述对象形状时优先使用;支持声明合并和 extends 继承;更适合面向对象风格
  • type:联合类型、交叉类型、元组、基本类型别名只能用 type;函数式编程风格更常用
  • 实际项目中混用很常见,关键是团队保持一致

🔗 函数类型与回调

TypeScript 可以精确描述回调函数和高阶函数的类型签名。

// 用 type 定义函数类型
type Callback = (data: string) => void;
type Predicate<T> = (item: T) => boolean;
type Transformer<T, U> = (input: T) => U;

// 使用函数类型
function fetchData(url: string, onSuccess: Callback, onError: Callback): void {
    // ...
}

// 高阶函数:接收函数,返回函数
function filter<T>(arr: T[], predicate: Predicate<T>): T[] {
    return arr.filter(predicate);
}

const numbers = [1, 2, 3, 4, 5];
const evens = filter(numbers, n => n % 2 === 0);  // [2, 4]

// 工厂函数模式
type Logger = (message: string) => void;

function createLogger(prefix: string): Logger {
    return (message: string) => {
        console.log(`[${prefix}] ${message}`);
    };
}

const appLog = createLogger("APP");
appLog("启动完成");  // "[APP] 启动完成"

💡 接口也能描述函数:

interface SearchFunc {
    (source: string, keyword: string): boolean;
}

const search: SearchFunc = (src, kw) => src.includes(kw);

但在实践中,用 type 定义函数类型更简洁,也更常见。

📝 本章要点

函数类型

参数和返回值都可标注类型;支持可选参数 ?、默认值 = 和剩余参数 ...

函数重载

多个重载签名 + 一个实现签名;用于精确描述输入输出的映射关系

interface

描述对象形状;支持可选/只读属性、索引签名、继承和声明合并

type 别名

比 interface 更灵活:联合类型、交叉类型、工具类型都需要 type

Duck Typing

TypeScript 的接口是结构化类型检查,无需显式 implements 声明

回调与高阶函数

用 type 定义函数签名类型,让回调参数获得完整的类型检查