Skip to content
第 107 / 250 章前端⏱ 10 分钟阅读

第 107 章:命名空间与声明合并

学习目标

  • 理解 namespace 的使用场景
  • 掌握声明合并的各种形式
  • 学会扩展第三方库类型
  • 区分 namespace 与 module 的边界

一、namespace 基础

namespace 是 TS 早期为内部模块设计的一种组织代码的方式,类似其他语言的「包」概念。

1.1 基本用法

typescript
namespace Utils {
  export function formatDate(d: Date): string {
    return d.toISOString();
  }

  export class StringHelper {
    static capitalize(s: string): string {
      return s[0].toUpperCase() + s.slice(1);
    }
  }

  export const VERSION = '1.0.0';
}

// 使用
Utils.formatDate(new Date());
Utils.StringHelper.capitalize('hello');
Utils.VERSION;

1.2 嵌套 namespace

typescript
namespace App {
  export namespace Components {
    export class Button {}
    export class Modal {}
  }

  export namespace Utils {
    export function log() {}
  }
}

// 使用
App.Components.Button;
App.Utils.log();

1.3 跨文件 namespace

typescript
// a.ts
namespace App {
  export class A {}
}

// b.ts
namespace App {
  export class B {}
}

// 使用
namespace App {
  const a = new A();
  const b = new B();
}

注意

跨文件 namespace 需要在编译时按顺序合并文件,已经过时。现代项目优先用 ES Module。

二、namespace vs module

typescript
// namespace(命名空间)
namespace MyApp {
  export const foo = 1;
}

// module(模块)
export const foo = 1;
特性namespacemodule
文件通常单文件一个文件即一个模块
现代项目❌ 不推荐✅ 推荐
适用场景全局类型扩展、声明合并业务代码
编译产物立即执行按需加载

现代项目建议

优先用 ES Module + 文件目录组织。namespace 仅在以下场景使用:

  1. 全局类型扩展
  2. 第三方库类型增强

三、声明合并(Declaration Merging)

声明合并是 TS 的核心特性:同名的多个声明自动合并

3.1 接口合并

typescript
interface User {
  name: string;
}

interface User {
  age: number;
}

// 合并后
interface User {
  name: string;
  age: number;
}

const u: User = { name: 'Tom', age: 18 };

3.2 namespace 合并

typescript
namespace Animals {
  export class Cat {}
}

namespace Animals {
  export class Dog {}
  export const count = 0;
}

// 合并后
namespace Animals {
  export class Cat {}
  export class Dog {}
  export const count: 0;
}

3.3 namespace + interface 合并

typescript
// 声明:把函数和接口合并
interface MyFunction {
  (x: number): string;
  description: string;
}

function MyFunction(x: number): string {
  return x.toString();
}
MyFunction.description = 'test';

// 也可以用 namespace + function
function greet(name: string): string {
  return `Hello, ${name}`;
}

namespace greet {
  export const description = 'greeting function';
}

greet('Tom');     // 调用
greet.description;  // 访问属性

3.4 class + interface 合并

typescript
class Box {
  contents: string = '';
}

interface Box {
  // 给类添加新方法(类型层面)
  serialize(): string;
}

const b = new Box();
b.contents = 'hello';
b.serialize();  // ✅ 类型上有这个方法
// 但运行时需要手动实现:Box.prototype.serialize = function() { ... }

四、模块扩展(Module Augmentation)

模块扩展是声明合并的一种特殊形式

4.1 扩展第三方模块

typescript
// 给 express 的 Request 加 user 字段
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: number;
        name: string;
      };
    }
  }
}

export {};

// 使用:req.user 自动有类型
app.get('/profile', (req, res) => {
  console.log(req.user?.name);
});

4.2 给已有模块加导出

typescript
// 假设 lodash 没有 myUtil,我们加上
import 'lodash';

declare module 'lodash' {
  interface LoDashStatic {
    myUtil(input: string): string;
  }
}

// 现在 lodash 有 myUtil 了
_.myUtil('hello');

4.3 扩展 React 模块

typescript
// 给 JSX 加自定义属性
declare module 'react' {
  interface HTMLAttributes<T> {
    'data-testid'?: string;
    'data-cy'?: string;
  }
}

// 现在 div 自动有这些属性
<div data-testid="main" />  // ✅ 有类型

五、声明合并实战

5.1 给全局 window 加方法

typescript
// globals.d.ts
export {};

declare global {
  interface Window {
    myApp: {
      version: string;
      track(event: string, data?: object): void;
    };
  }
}

// 使用
window.myApp.track('click', { button: 'submit' });

5.2 给已有类加方法(仅类型)

typescript
// augment.d.ts
declare global {
  interface Array<T> {
    toMyFormat(): string;
  }
}

export {};

// 使用(类型上有,运行时需要自己实现)
[1, 2, 3].toMyFormat();

5.3 扩展第三方 UI 库

typescript
// 扩展 Element Plus 组件属性
declare module 'element-plus' {
  interface ButtonProps {
    customProp?: string;
  }
}

export {};

六、namespace 的实际应用

虽然不推荐用于业务代码,但 namespace 在类型声明文件中仍有价值。

6.1 类型声明中的 namespace

typescript
// chart.d.ts
declare namespace ChartLib {
  interface Config {
    type: string;
    data: any[];
  }

  class Chart {
    constructor(config: Config);
    render(): void;
  }

  function create(config: Config): Chart;
}

declare module 'chart-lib' {
  export = ChartLib;
}

6.2 全局工具函数

typescript
// utils.d.ts
declare namespace MyApp {
  function formatDate(d: Date): string;
  function parseDate(s: string): Date;
}

// 使用
MyApp.formatDate(new Date());

七、模块声明 vs 命名空间

7.1 模块声明更现代

typescript
// ✅ 推荐:用 ES Module
// utils/index.ts
export function formatDate(d: Date): string { /* ... */ }
export function parseDate(s: string): Date { /* ... */ }

// 使用
import { formatDate, parseDate } from '@/utils';

7.2 命名空间仍有用的场景

typescript
// 1. 全局类型扩展(必须用 declare global)
declare global {
  interface Window { foo: string; }
}

// 2. 老的库用 namespace 组织类型(向后兼容)
declare namespace jQuery {
  interface AjaxSettings { /* ... */ }
  function ajax(settings: AjaxSettings): void;
}

// 3. 声明合并(只能用 namespace)
function greet(s: string): string { return s; }
namespace greet {
  export const version = '1.0';
}

八、namespace 与 module 的混用

typescript
// types.ts
export namespace MyTypes {
  export interface User {
    id: number;
    name: string;
  }

  export type Status = 'active' | 'inactive';
}

// 使用
import { MyTypes } from './types';
const user: MyTypes.User = { id: 1, name: 'Tom' };

注意

这种写法不被推荐,应该用单独的 type 和 interface。

九、实战:声明文件中的 namespace

9.1 jQuery 风格库

typescript
// jquery.d.ts
declare namespace JQuery {
  interface AjaxSettings {
    url: string;
    method?: 'GET' | 'POST';
    data?: any;
  }

  interface Promise<T> {
    done(callback: (data: T) => void): this;
    fail(callback: (error: any) => void): this;
  }

  function ajax<T = any>(settings: AjaxSettings): Promise<T>;
  function $(selector: string): HTMLElement;
}

declare module 'jquery' {
  export = JQuery;
}

9.2 浏览器全局库

typescript
// chartjs.d.ts(挂载到 window.Chart)
declare namespace Chart {
  interface Config {
    type: string;
    data: any;
  }
  class Chart {
    constructor(ctx: CanvasRenderingContext2D, config: Config);
    update(): void;
  }
}

interface Window {
  Chart: typeof Chart;
}

十、声明合并的限制

10.1 不能合并 class

typescript
class User {}
class User {}  // ❌ Duplicate identifier 'User'

// ✅ 用 namespace 扩展
class User {}
namespace User {
  export const version = '1.0';
}
// 这是合法的(给类加静态属性)

10.2 函数不能合并(只能 namespace 扩展)

typescript
function foo() {}
function foo() {}  // ❌ Duplicate function implementation

// ✅ 用 namespace 添加属性
function foo() {}
namespace foo {
  export const extra = 1;
}

10.3 enum 不能合并

typescript
enum Color { Red }
enum Color { Blue }  // ❌ Duplicate identifier 'Color'

十一、常见错误

11.1 在 ES Module 中使用 namespace

typescript
// ❌ 不推荐:用 namespace 组织业务代码
namespace UserService {
  export function getUser() {}
}

// ✅ 推荐:用模块
// user-service.ts
export function getUser() {}

11.2 忘记 export {}

typescript
// ❌ 报错
declare global {
  interface Window { foo: string; }
}
// Augmentations for the global scope can only be directly nested in external modules

// ✅ 解决
declare global {
  interface Window { foo: string; }
}
export {};

11.3 namespace 跨文件顺序问题

typescript
// a.ts
namespace App {
  export const a = 1;
}

// b.ts
namespace App {
  export const b = a + 1;  // ⚠️ 依赖 a.ts 先编译
}

// 解决:用 import 替代
// b.ts
import './a';
namespace App {
  export const b = a + 1;
}

十二、本章小结

要点关键
namespace早期内部模块,现代项目不推荐
适用场景全局扩展、声明合并
声明合并同名 interface/namespace 自动合并
模块扩展declare module 给已有库加类型
全局扩展declare global 必须在模块文件中
class+interface给类加方法(仅类型层面)
优先选择ES Module + 文件组织

动手练习

  1. 全局扩展:扩展 Window 接口,添加自定义全局方法
  2. 模块扩展:给 lodash 加一个 myUtil 方法的类型
  3. 声明合并:定义两个同名 interface,验证自动合并
  4. namespace 函数扩展:给原生 Array 加一个 groupBy 方法的类型

推荐阅读


下一章第 108 章:tsconfig 详解

本站基于 VitePress 构建 · 由 Codebook 团队维护