TypeScript 完全指南(中):函数、接口、类与高级类型

承接上篇:掌握了基础类型后,我们来学习 TypeScript 中真正让代码变得可扩展、可复用的核心特性。

上篇我们学会了给变量、数组、对象标注类型。但如果你的代码只用到基础类型,那 TypeScript 的威力连十分之一都没发挥出来。真正让 TypeScript 与众不同的,是它能够描述函数的行为、定义对象的结构(接口)、实现面向对象编程(类)以及用高级类型进行类型编程。

本文将聚焦这些核心内容,全程纯 TypeScript,不涉及任何前端框架。

第四部分:函数------类型系统的"连接器"

函数是 JavaScript 的"一等公民",TypeScript 为函数提供了丰富的类型注解方式。

4.1 函数参数与返回值的类型注解

最简单的函数类型注解包括参数类型和返回值类型:

typescript

复制代码
// 语法:(参数名: 类型, 参数名: 类型): 返回值类型
function add(x: number, y: number): number {
    return x + y;
}

const result = add(3, 5);  // result 被推断为 number

如果函数没有返回值,使用 void

typescript

复制代码
function logMessage(msg: string): void {
    console.log(msg);
    // 没有 return 语句,或 return undefined;
}

4.2 函数类型表达式

你可以用变量来存储函数,此时需要描述这个变量的类型:

typescript

复制代码
// 定义一个函数类型:接受两个 number,返回 number
let myAdd: (x: number, y: number) => number;

// 赋值一个符合该类型的函数
myAdd = function (a: number, b: number): number {
    return a + b;
};

// 也可以使用类型别名简化
type MathOperation = (a: number, b: number) => number;
let multiply: MathOperation = (x, y) => x * y;

4.3 可选参数和默认参数

TypeScript 中的每个函数参数都是必传的,除非标记为可选或提供默认值。

可选参数 :使用 ? 标记,必须放在必选参数的后面。

typescript

复制代码
function buildName(firstName: string, lastName?: string): string {
    if (lastName) {
        return `${firstName} ${lastName}`;
    }
    return firstName;
}

console.log(buildName("Alice"));        // "Alice"
console.log(buildName("Alice", "Chen")); // "Alice Chen"
// console.log(buildName("Alice", "Chen", "Jr")); // ❌ 参数过多

默认参数:为参数提供默认值,此时参数自动变为可选。

typescript

复制代码
function greet(name: string, greeting: string = "Hello"): string {
    return `${greeting}, ${name}!`;
}

console.log(greet("Bob"));          // "Hello, Bob!"
console.log(greet("Bob", "Hi"));    // "Hi, Bob!"

4.4 剩余参数

使用 ...rest: Type[] 语法接收任意数量的参数:

typescript

复制代码
function sumNumbers(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sumNumbers(1, 2, 3, 4, 5));  // 15
console.log(sumNumbers(10, 20));         // 30

4.5 函数重载

函数重载允许同一个函数根据不同的参数类型或数量,执行不同的逻辑,并提供准确的类型提示。

重载的语法:先写多个重载签名(只有类型,没有实现),然后写一个通用的实现签名。

typescript

复制代码
// 重载签名
function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: boolean): string;

// 实现签名(必须兼容所有重载)
function formatValue(value: string | number | boolean): string {
    if (typeof value === "string") {
        return `"${value}"`;
    } else if (typeof value === "number") {
        return value.toFixed(2);
    } else {
        return value ? "true" : "false";
    }
}

console.log(formatValue("hello"));  // 输出 "hello"
console.log(formatValue(3.1415));   // 输出 3.14
console.log(formatValue(true));     // 输出 true

注意:TypeScript 会根据你传入的参数类型,匹配到对应的重载签名,从而给出准确的返回类型。上面的例子中,formatValue("hi") 的返回类型被推断为 string

4.6 函数中的 this 类型

在 JavaScript 中,this 的指向容易出错。TypeScript 允许你在函数参数中声明 this 的类型(第一个参数,名字必须叫 this)。

typescript

复制代码
interface User {
    name: string;
    age: number;
}

function describe(this: User): void {
    console.log(`${this.name} is ${this.age} years old`);
}

const user: User = { name: "Alice", age: 30 };
describe.call(user);  // 正确调用
// describe();        // ❌ 错误:this 上下文丢失

通常你不需要手动标注 this,但在事件监听器、类方法等场景中非常有用。

第五部分:接口------定义对象的形状

接口是 TypeScript 中最核心的概念之一,它用于定义对象的结构(即这个对象应该有哪些属性,每个属性的类型是什么)。

5.1 基本接口语法

typescript

复制代码
interface Person {
    name: string;
    age: number;
    email: string;
}

// 使用接口
const alice: Person = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"
};

// 缺少属性会报错
// const bob: Person = { name: "Bob", age: 30 };  // ❌ 缺少 email

5.2 可选属性和只读属性

  • 可选属性 :用 ? 标记,表示该属性可以不存在。

  • 只读属性 :用 readonly 标记,表示初始化后不可修改。

typescript

复制代码
interface Product {
    readonly id: number;   // 只读
    name: string;
    price: number;
    description?: string;  // 可选
}

const phone: Product = {
    id: 1001,
    name: "Smartphone",
    price: 599
    // description 可以省略
};
// phone.id = 1002;        // ❌ 错误:id 是只读的
phone.price = 499;         // ✅ 可以修改非只读属性

5.3 多余属性检查

当将一个对象字面量直接赋值给接口类型的变量时,TypeScript 会进行多余属性检查:不允许出现接口中未定义的属性。

typescript

复制代码
interface Point {
    x: number;
    y: number;
}

// 错误:对象字面量只能指定已知属性
// const p: Point = { x: 10, y: 20, z: 30 };  // ❌ 'z' 不在 Point 中

// 绕过多余属性检查的几种方式:
// 1. 使用类型断言
const p1 = { x: 10, y: 20, z: 30 } as Point;

// 2. 先赋值给另一个变量
const temp = { x: 10, y: 20, z: 30 };
const p2: Point = temp;  // ✅ 没有直接使用字面量,不会触发多余属性检查

// 3. 使用索引签名(见下文)

5.4 索引签名

当你不确定对象会有哪些属性,但知道属性值的类型时,可以使用索引签名。

typescript

复制代码
// 字符串索引签名:所有属性值必须是 string
interface StringDictionary {
    [key: string]: string;
}

const dict: StringDictionary = {
    hello: "你好",
    world: "世界"
    // age: 25   // ❌ 错误:数字不能赋给 string
};

// 数字索引签名(通常用于类数组对象)
interface NumberArray {
    [index: number]: string;
}
const arr: NumberArray = ["a", "b", "c"];
console.log(arr[0]);  // "a"

注意:同时存在字符串索引和数字索引时,数字索引的返回值类型必须是字符串索引返回值类型的子类型(因为 JavaScript 会将数字索引转换为字符串)。

5.5 函数类型接口

接口不仅可以描述对象,还可以描述函数类型:

typescript

复制代码
interface GreetFunction {
    (name: string, greeting?: string): string;
}

const greet: GreetFunction = (name, greeting = "Hello") => {
    return `${greeting}, ${name}!`;
};

5.6 接口继承

接口可以继承另一个或多个接口,把它们的属性合并过来。

typescript

复制代码
interface Animal {
    name: string;
    age: number;
}

interface Dog extends Animal {
    breed: string;
    bark(): void;
}

const myDog: Dog = {
    name: "Buddy",
    age: 3,
    breed: "Golden Retriever",
    bark() {
        console.log("Woof!");
    }
};

// 多继承
interface Swimmer {
    swim(): void;
}
interface Flyer {
    fly(): void;
}
interface Duck extends Swimmer, Flyer {
    quack(): void;
}

5.7 接口与类型别名的区别

回顾上篇我们讲了类型别名(type),它也能描述对象形状。什么时候用接口,什么时候用类型别名?

特性 interface type
声明合并 ✅ 支持(多次定义同名接口会自动合并) ❌ 不支持
继承/扩展 extends 语法 交叉类型 &
描述对象/函数 ✅ 专门为此设计 ✅ 也可以
描述联合类型 ❌ 不能 ✅ 可以
描述元组 ❌ 不能(TS 4.2+ 可以部分用,但不推荐) ✅ 可以
实现(implements) 类可以实现多个接口 类不能实现类型别名(但可以 implements 交叉类型)

经验法则 :默认使用 interface,直到你需要使用 type 的独有特性(如联合类型、映射类型等)。

typescript

复制代码
// 声明合并示例
interface User {
    name: string;
}
interface User {
    age: number;
}
// 最终 User 接口包含 name 和 age
const u: User = { name: "Alice", age: 25 };

第六部分:类------面向对象的 TypeScript

TypeScript 对 ES6 类进行了增强,加入了访问修饰符、抽象类、只读属性等面向对象特性。

6.1 基础类语法

typescript

复制代码
class Animal {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    speak(): void {
        console.log(`${this.name} makes a sound.`);
    }
}

const cat = new Animal("Whiskers", 2);
cat.speak();

6.2 访问修饰符

TypeScript 提供了三个访问修饰符:

  • public:默认,任何地方都可访问。

  • private:只能在类内部访问。

  • protected:能在类内部及子类中访问,但不能在实例中访问。

typescript

复制代码
class Person {
    public name: string;      // 公开
    private age: number;      // 私有
    protected id: number;     // 受保护

    constructor(name: string, age: number, id: number) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public getAge(): number {
        return this.age;       // 内部可以访问 private
    }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, age: number, id: number, department: string) {
        super(name, age, id);
        this.department = department;
    }

    public getInfo(): string {
        // return this.age;    // ❌ private,不能访问
        return `${this.name} (ID: ${this.id})`; // ✅ protected 可以访问
    }
}

const emp = new Employee("Bob", 30, 12345, "IT");
console.log(emp.name);          // ✅ public
// console.log(emp.age);        // ❌ private
// console.log(emp.id);         // ❌ protected

6.3 参数属性(Parameter Properties)

TypeScript 提供了一种简写:在构造函数参数前加上修饰符,可以自动声明并初始化同名字段。

typescript

复制代码
class User {
    // 相当于声明了 public name: string,并在构造函数中赋值
    constructor(public name: string, private age: number) {}
}

const u = new User("Alice", 25);
console.log(u.name);  // ✅
// console.log(u.age); // ❌ private

6.4 只读属性

使用 readonly 关键字,属性只能在初始化时赋值,之后不可修改。

typescript

复制代码
class Book {
    readonly isbn: string;
    title: string;

    constructor(isbn: string, title: string) {
        this.isbn = isbn;
        this.title = title;
    }
}

const book = new Book("123456", "TypeScript Guide");
// book.isbn = "654321";  // ❌ 错误
book.title = "New Title";  // ✅ 可以修改非 readonly 属性

6.5 抽象类

抽象类不能直接实例化,必须被继承。它可以包含抽象方法(没有方法体,子类必须实现)和具体方法。

typescript

复制代码
abstract class Shape {
    abstract getArea(): number;   // 抽象方法,没有实现

    describe(): string {          // 具体方法,可以被子类继承
        return `This shape has area: ${this.getArea()}`;
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super();
    }

    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Square extends Shape {
    constructor(private side: number) {
        super();
    }

    getArea(): number {
        return this.side ** 2;
    }
}

// const s = new Shape();  // ❌ 错误:抽象类不能实例化
const circle = new Circle(5);
console.log(circle.getArea());      // 78.539...
console.log(circle.describe());     // "This shape has area: 78.539..."

6.6 类实现接口

类可以使用 implements 关键字来实现一个或多个接口,强制类包含接口中定义的所有属性/方法。

typescript

复制代码
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void;
}

class Clock implements ClockInterface {
    currentTime: Date;

    constructor(h: number, m: number) {
        this.currentTime = new Date();
    }

    setTime(d: Date): void {
        this.currentTime = d;
    }
}

类也可以实现多个接口:

typescript

复制代码
interface Printable {
    print(): void;
}
interface Serializable {
    serialize(): string;
}

class Document implements Printable, Serializable {
    print(): void {
        console.log("Printing...");
    }
    serialize(): string {
        return "document data";
    }
}

6.7 静态成员

静态属性和方法属于类本身,而不是实例。使用 static 关键字。

typescript

复制代码
class MathUtils {
    static PI: number = 3.14159;

    static circleArea(radius: number): number {
        return this.PI * radius ** 2;
    }
}

console.log(MathUtils.PI);           // 3.14159
console.log(MathUtils.circleArea(5)); // 78.53975

第七部分:高级类型------类型编程的艺术

前面我们学了基础类型、函数、接口和类。这一部分将进入 TypeScript 的"高级玩法":交叉类型、类型保护、类型操作符、映射类型、条件类型等。

7.1 交叉类型(&

交叉类型将多个类型合并为一个类型,包含所有类型的成员。常用于混入(mixin)。

typescript

复制代码
interface A {
    a: string;
}
interface B {
    b: number;
}
type C = A & B;  // 既有 a 又有 b

const obj: C = {
    a: "hello",
    b: 42
};

// 更复杂的例子:合并函数签名
type Admin = { name: string; privileges: string[] };
type Employee = { name: string; startDate: Date };
type ElevatedEmployee = Admin & Employee;

const e: ElevatedEmployee = {
    name: "Alice",
    privileges: ["create-server"],
    startDate: new Date()
};

注意:交叉类型可能会产生不可调和的矛盾,比如 string & number 会变成 never

7.2 类型保护(Type Guards)

类型保护是运行时检查,用于在特定作用域内收窄类型。

typeof 类型保护

typescript

复制代码
function padLeft(value: string | number, padding: number): string {
    if (typeof value === "string") {
        // 这里 value 是 string
        return value.padStart(padding, " ");
    } else {
        // 这里 value 是 number
        return value.toString().padStart(padding, " ");
    }
}

instanceof 类型保护

typescript

复制代码
class Bird {
    fly() { console.log("Flying"); }
}
class Fish {
    swim() { console.log("Swimming"); }
}

function move(animal: Bird | Fish) {
    if (animal instanceof Bird) {
        animal.fly();
    } else {
        animal.swim();
    }
}

自定义类型保护 :使用 value is Type 语法定义函数,返回布尔值,告诉 TS 类型收窄。

typescript

复制代码
interface Cat {
    meow(): void;
}
interface Dog {
    bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function makeSound(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();  // TS 知道这里是 Cat
    } else {
        animal.bark();  // TS 知道这里是 Dog
    }
}

7.3 类型操作符 keyof

keyof 操作符获取一个类型的所有属性名组成的联合类型。

typescript

复制代码
interface Person {
    name: string;
    age: number;
    address: string;
}
type PersonKeys = keyof Person;  // "name" | "age" | "address"

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const p: Person = { name: "Alice", age: 25, address: "123 St" };
const nameValue = getProperty(p, "name");  // string
// const invalid = getProperty(p, "invalid"); // ❌ 错误

7.4 索引访问类型(T[K]

你可以像访问数组元素一样,获取类型的某个属性的类型。

typescript

复制代码
interface Car {
    brand: string;
    year: number;
    owner: { name: string; age: number };
}

type BrandType = Car["brand"];       // string
type OwnerType = Car["owner"];       // { name: string; age: number }
type OwnerAgeType = Car["owner"]["age"]; // number

// 联合索引
type Keys = "brand" | "year";
type Values = Car[Keys];  // string | number

7.5 映射类型(Mapped Types)

映射类型基于旧类型创建新类型,语法:{ [P in K]: T[P] }

内置的几个常用映射类型:

typescript

复制代码
// Partial<T>:所有属性变为可选
type PartialPerson = Partial<Person>;
// { name?: string; age?: number; address?: string; }

// Readonly<T>:所有属性变为只读
type ReadonlyPerson = Readonly<Person>;
// { readonly name: string; readonly age: number; readonly address: string; }

// Pick<T, K>:选取部分属性
type NameAndAge = Pick<Person, "name" | "age">;
// { name: string; age: number; }

// Record<K, T>:创建一个对象类型,键为 K,值为 T
type Page = "home" | "about" | "contact";
type PageInfo = Record<Page, { title: string; url: string }>;
// 等价于:
// { home: {title, url}; about: {...}; contact: {...} }

你可以自己实现这些工具类型:

typescript

复制代码
type MyPartial<T> = {
    [P in keyof T]?: T[P];
};

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};

7.6 条件类型(Conditional Types)

条件类型的语法类似于三元运算符:T extends U ? X : Y

typescript

复制代码
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;  // true
type B = IsString<42>;       // false

// 使用 infer 提取类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function example(): number[] { return [1, 2, 3]; }
type R = ReturnType<typeof example>;  // number[]

// 递归条件类型
type Flatten<T> = T extends any[] ? T[number] : T;
type NumArr = Flatten<number[]>;   // number
type Str = Flatten<string>;        // string

7.7 内置工具类型速查

TypeScript 提供了很多实用的工具类型,下面列举最常用的:

工具类型 作用 示例
Partial<T> 将 T 的所有属性变为可选 Partial<Person>
Required<T> 将 T 的所有属性变为必选 Required<PartialPerson>
Readonly<T> 将 T 的所有属性变为只读 Readonly<Person>
Pick<T, K> 从 T 中选取指定的属性集 K Pick<Person, "name">
Omit<T, K> 从 T 中排除指定的属性集 K Omit<Person, "age">
Exclude<T, U> 从 T 中排除可赋值给 U 的类型 `Exclude<"a"
Extract<T, U> 从 T 中提取可赋值给 U 的类型 `Extract<"a"
NonNullable<T> 从 T 中排除 null 和 undefined `NonNullable<string
ReturnType<T> 获取函数 T 的返回值类型 ReturnType<()=>number>number
Parameters<T> 获取函数 T 的参数类型元组 Parameters<(a: string)=>void>[string]
InstanceType<T> 获取构造函数 T 的实例类型 InstanceType<typeof Person>Person

示例用法:

typescript

复制代码
interface Todo {
    title: string;
    description: string;
    completed: boolean;
    createdAt: Date;
}

// 创建一个只包含标题和完成状态的类型
type TodoPreview = Pick<Todo, "title" | "completed">;

// 创建一个不包含描述的类型
type TodoWithoutDesc = Omit<Todo, "description">;

// 将 Date 属性变为可选
type TodoWithOptionalDate = Partial<Pick<Todo, "createdAt">> & Omit<Todo, "createdAt">;

小结与下篇预告

本文(中篇)涵盖了

  • 函数:参数、返回值、可选/默认参数、剩余参数、重载

  • 接口:对象形状、可选/只读属性、索引签名、继承、与 type 的区别

  • 类:访问修饰符、参数属性、抽象类、实现接口、静态成员

  • 高级类型:交叉类型、类型保护、keyof、索引访问、映射类型、条件类型、内置工具类型

下篇(终篇)预告

  • 泛型的深入(泛型约束、泛型类、泛型与接口/类的结合)

  • 模块与命名空间

  • 声明文件(d.ts

  • 配置文件的深度解析

  • 实际项目中的最佳实践与性能优化

如果你已经掌握了中篇的内容,你完全有能力用 TypeScript 编写出结构清晰、类型安全的程序。请继续关注下篇,我们将把 TypeScript 的能力推向极致。

相关推荐
鹏多多1 小时前
OpenSpec+SDD规范驱动AI Agent开发项目实战指南
前端·vue.js·react.js
叶小树咯1 小时前
React 为什么不能像 Vue 那样 state.count++
前端·react.js
ricardo19731 小时前
防抖节流进阶 + requestAnimationFrame:滚动与输入场景的性能优化
前端·面试
wjj不想说话1 小时前
你项目里的 Pinia,可能已经成了第二个 localStorage
前端·vue.js
wuhen_n1 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·langchain·ai编程
天蓝色的鱼鱼2 小时前
画1万个图形就卡成PPT?试试这款国产高性能2D引擎
前端·javascript
云水一下2 小时前
JavaScript 从零基础到精通系列:异步编程与网络请求
前端·javascript
卡卡军2 小时前
🌈 react-sketch-ruler v3 升级之旅:当 React 遇上跨框架标尺引擎
前端·react.js
Asmewill2 小时前
DeepAgents学习笔记三(Backend记忆存储)
前端