Skip to content

泛型、装饰器、interface 与 type、命名空间

按泛型 → 装饰器 → interface/type → 命名空间的常见顺序编排;各篇可独立阅读。


第一部分:泛型

泛型(Generics)是 TS 中用于创建可复用、类型安全的组件的核心特性

它的核心思想是:在定义函数、类、接口时,不预先指定具体类型,而是在使用时再确定类型。

可以把泛型理解为「类型的占位符 / 变量」—— 就像函数的参数接收值,泛型参数接收「类型」

泛型函数(最常用)

ts
/********************* 单泛型 ***********************/

// 定义泛型函数
function returnValue<T>(arg: T): T {
  return arg;
}

// 使用时指定类型(显式)
const num = returnValue<number>(10); // num 类型是 number
const str = returnValue<string>('hello'); // str 类型是 string

// 使用时自动推导类型(隐式,更常用)
const bool = returnValue(true); // bool 类型是 boolean
const arr = returnValue([1, 2, 3]); // arr 类型是 number[]

/********************* 多泛型 ***********************/

// 合并两个对象:T 是第一个对象类型,U 是第二个对象类型
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const user = merge({ name: '张三' }, { age: 20 });
// user 类型是 { name: string } & { age: number }
console.log(user.name, user.age); // 类型提示正常

泛型接口

ts
// 定义泛型接口:表示“键值对”结构,键是 string,值是泛型 T
interface KeyValue<T> {
  key: string;
  value: T;
}

// 使用时指定类型
const numKV: KeyValue<number> = { key: 'age', value: 20 };
const strKV: KeyValue<string> = { key: 'name', value: '张三' };

泛型类

ts
// 泛型类:栈(先进后出)
class Stack<T> {
  private items: T[] = [];

  // 入栈
  push(item: T): void {
    this.items.push(item);
  }

  // 出栈
  pop(): T | undefined {
    return this.items.pop();
  }

  // 获取栈长度
  size(): number {
    return this.items.length;
  }
}

// 使用数字类型的栈
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2(类型是 number | undefined)

// 使用字符串类型的栈
const strStack = new Stack<string>();
strStack.push('a');
strStack.push('b');
console.log(strStack.pop()); // 'b'(类型是 string | undefined)

泛型约束 extends | extends keyof

默认情况下,泛型 T 可以是任意类型,但有时我们需要限制 T 的范围(比如要求 T 必须包含某个属性),这就是「泛型约束」。

ts
/******************** 示例一:要求参数必须有 length 属性 *************************/

// 定义约束接口:包含 length 属性
interface HasLength {
  length: number;
}

// 泛型 T 继承 HasLength,限制 T 必须有 length 属性
function getLength<T extends HasLength>(arg: T): number {
  return arg.length;
}

// 合法:string 有 length 属性
console.log(getLength('hello')); // 5

// 合法:数组有 length 属性
console.log(getLength([1, 2, 3])); // 3

// 不合法:number 没有 length 属性(TS 会报错)
// console.log(getLength(123));

/******************** 示例二:约束泛型为对象的键 *************************/

// 获取对象的属性值:K 必须是 T 的键(keyof T 表示 T 的所有键的联合类型)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: '张三', age: 20 };
// 合法:"name" 是 user 的键
console.log(getProperty(user, 'name')); // 张三(类型是 string)

// 合法:"age" 是 user 的键
console.log(getProperty(user, 'age')); // 20(类型是 number)

// 不合法:"gender" 不是 user 的键(TS 报错)
// console.log(getProperty(user, "gender"));

泛型默认值

ts
// 泛型默认值:T 默认为 string
function createArray<T = string>(length: number, value: T): T[] {
  return new Array(length).fill(value);
}

// 不指定类型,T 用默认值 string
const strArr = createArray(3, 'a'); // strArr 类型是 string[]

// 指定类型,覆盖默认值
const numArr = createArray<number>(3, 1); // numArr 类型是 number[]

泛型工具类型(TS 内置)

shell
工具类型     作用                           示例
Partial<T>    T 的所有属性变为可选         Partial<{ name: string }> { name?: string }
Required<T>    T 的所有属性变为必填         Required<{ name?: string }> { name: string }
Readonly<T>    T 的所有属性变为只读        Readonly<{ name: string }> { readonly name: string }
Pick<T, K> T 中挑选 K 个属性           Pick<{ name: string, age: number }, "name"> { name: string }
Omit<T, K> T 中排除 K 个属性           Omit<{ name: string, age: number }, "age"> { name: string }
Record<K, T> 创建键为 K、值为 T 的对象类型   Record<"name" | "age", string> { name: string; age: string }
ts
// 示例:使用 Partial 简化类型

interface User {
  name: string;
  age: number;
}

// 需求:更新用户信息,允许只传部分属性
type PartialUser = Partial<User>;
// PartialUser 等价于 { name?: string; age?: number }

function updateUser(user: User, changes: PartialUser): User {
  return { ...user, ...changes };
}

const oldUser = { name: '张三', age: 20 };
const newUser = updateUser(oldUser, { age: 21 }); // 只更新 age

第二部分:装饰器

使用装饰器可以让我们实现 AOP 编程(面向切面编程)(拦截器思维)、可以提升架构思维和架构能力、Nest、mideway 知名框架大量使用了装饰器

定义:装饰器就是一个方法,可以注入到类、方法、属性、参数上,扩展其功能

初始化 装饰器 环境

js
1. tsc --init
2. 修改 tsconfig.json 配置文件
  "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
  "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
3. yarn global add ts-node
4. 新建 index.ts

示例

js
ts-node index.ts
  1. 不传参数

结果:装饰器函数会执行,且默认把类作为参数传入装饰器工厂函数

js
// 不传参
function FirstDecorator(Target: any) {
  console.log("I am decorator");
  console.log("====================================");
  const target = new Target();
  target.read();
}

@FirstDecorator
class Person {
  name = "zhangsan";
  read() {
    console.log("i am class method");
    console.log("====================================");
  }
}
  1. 传参数

结果:在不传参数的基础上,可以给装饰器函数传入参数

js
// 传参
function FirstDecorator(param: string) {
  console.log("I am param", param);
  console.log("====================================");
  return function (Target: any) {
    console.log("I am decorator");
    console.log("====================================");
    const target = new Target();
    target.read();
  };
}

@FirstDecorator("hello world")
class Person {
  name = "zhangsan";
  read() {
    console.log("i am class method");
    console.log("====================================");
  }
}

装饰器分类

类装饰器、属性装饰器、方法装饰器、访问器装饰器、参数装饰器

装饰器的执行顺序:属性装饰器 -- 方法参数装饰器 -- 方法装饰器 -- 构造器参数装饰器 -- 类装饰器

多个类装饰器的执行顺序是:倒着执行,从下往上执行

js
// 类装饰器
@classDecorator
class Bird {
  // 属性装饰器
  @propertyDecorator
  name: string;

  // 方法装饰器
  @methodDecorator
  fly(
    // 参数装饰器
    @parameterDecorator
    meters: number
  ) {}

  // 访问器装饰器
  @accessorDecorator
  get egg() {}
}

第三部分:interface 与 type

在 TypeScript 中,type 和 interface 都用于定义自定义类型,但它们有一些关键区别和各自适用的场景。

interface 接口

专门用于定义对象 / 类的形状,语法更面向对象,只能定义对象类型

ts
// 定义对象接口
interface Person {
  name: string;
  age: number;
  sayHi?: () => void; // 可选属性
}

// 接口继承(支持多继承)
interface Student extends Person {
  studentId: string;
}

// 接口合并(同名接口会自动合并)
interface Person {
  gender: string; // 合并后 Person 包含 name/age/sayHi/gender
}

type 类型别名

可定义任意类型(对象、基本类型、联合类型、交叉类型等),语法更灵活

ts
// 定义基本类型别名
type Age = number;

// 定义对象类型
type Person = {
  name: string;
  age: number;
  sayHi?: () => void;
};

// 定义联合类型(interface 无法实现)
type Status = "success" | "error" | "pending";

// 定义交叉类型
type Student = Person & { studentId: string };

// 类型别名不可合并(同名会报错)
// type Person = { gender: string }; // 报错:标识符“Person”重复

二者区别

shell
特性                  interface                      type
定义范围               仅支持对象 / 类类型               支持所有类型(基本、联合、交叉等)
扩展方式               通过 extends 继承                通过 & 交叉类型实现
类实现                 类可通过 implements 实现接口      不能
合并声明               同名自动合并                      不支持合并,同名会报错
应用类型               只能用于对象类型                   定义任意类型(联合、元组等)

使用建议

  • 定义对象 / 类的结构,且可能需要扩展 / 合并 → 优先用 interface;
  • 定义基本类型、联合类型、交叉类型、元组 → 必须用 type;
  • 团队协作时,保持风格统一(比如 React 组件 Props 统一用 interface)。

总结

  • interface 专注于对象 / 类的形状定义,支持继承和声明合并,更贴合面向对象编程;
  • type 是通用的类型别名,支持所有类型定义,功能更灵活但不支持合并;
  • 核心选择逻辑:对象结构优先 interface,非对象类型(联合 / 交叉 / 基本类型)必须用 type。

第四部分:命名空间

在 TypeScript 中,命名空间(Namespace)是一种组织代码的方式,用于将相关的接口、类、函数等封装在一起,避免全局作用域中的命名冲突。

它类似于模块,但在使用方式和场景上有一定区别(命名空间更偏向于在全局作用域中组织代码,而模块更适合文件级别的代码分离)

基础用法

命名空间使用 namespace 关键字定义,内部可以包含变量、函数、类、接口等:

命名空间内部的成员默认是 “私有” 的,仅在命名空间内部可见。若要让外部访问,必须使用 export 关键字导出

ts
// 定义命名空间
namespace MyNamespace {
  export const message = "Hello from namespace"; // 需要用 export 暴露外部可访问的成员

  export function logMessage() {
    console.log(message);
  }

  export class MyClass {
    greet() {
      console.log("Greetings!");
    }
  }

  // 未使用 export 的成员仅在命名空间内部可见
  const internalVar = "内部变量";
}

访问命名空间成员

通过 命名空间.成员名 的方式访问导出的成员

ts
// 访问命名空间中的成员
console.log(MyNamespace.message); // 输出:Hello from namespace
MyNamespace.logMessage(); // 输出:Hello from namespace

const instance = new MyNamespace.MyClass();
instance.greet(); // 输出:Greetings!

嵌套命名空间

命名空间可以嵌套,形成更细粒度的代码组织

ts
namespace OuterNamespace {
    export namespace InnerNamespace {
        export function hello() {
            console.log("Hello from inner namespace");
        }
    }
}

// 访问嵌套命名空间的成员
OuterNamespace.InnerNamespace.hello(); // 输出:Hello from inner namespace

总结

命名空间用于组织相关代码,避免命名冲突。

内部成员需用 export 导出才能被外部访问。

支持嵌套和合并。

现代项目中更推荐使用模块,命名空间适用于简单场景或兼容旧代码。