返回首页

说说你对 TypeScript 中模块的理解?

问题解析

模块是现代 TypeScript/JavaScript 开发的核心概念,考察候选人对 ES Module、CommonJS、模块解析策略以及模块声明的理解。

核心概念

  • 模块(Module):具有独立作用域的代码单元
  • ES Module:ES6 标准的模块系统(import/export
  • CommonJS:Node.js 的模块系统(require/module.exports
  • AMD/UMD:浏览器端模块规范
  • 模块解析:编译器如何查找模块
  • 声明文件:为无类型模块提供类型定义

详细解答

1. 模块是什么

TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。

相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的。

// 1.ts - 没有 export,全局作用域
const a = 1;

// 2.ts - 会报错:重复声明 "a"
const a = 1;

解决方式:添加 export 使其成为模块

// 1.ts
export const a = 1;

// 2.ts
export const a = 1; // ok,模块作用域隔离

2. 模块的导出

2.1 命名导出

// math.ts
export const PI = 3.14159;

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

export class Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}

// 或者统一导出
const PI = 3.14159;
function add(a: number, b: number): number {
  return a + b;
}
export { PI, add };

2.2 默认导出

// utils.ts
export default function greet(name: string): string {
  return `Hello, ${name}!`;
}

// 一个模块只能有一个默认导出
const config = { apiUrl: 'https://api.example.com' };
export default config;

2.3 类型导出

// types.ts
export type User = {
  id: number;
  name: string;
};

export interface Product {
  sku: string;
  price: number;
}

// 重新导出
export { User as UserType } from './user';

3. 模块的导入

3.1 命名导入

import { PI, add, Calculator } from './math';

console.log(PI);
console.log(add(1, 2));
const calc = new Calculator();

3.2 默认导入

import greet from './utils';
// 或者
import greetFn from './utils';

console.log(greet('World'));

3.3 命名空间导入

import * as math from './math';

console.log(math.PI);
console.log(math.add(1, 2));

3.4 混合导入

import React, { useState, useEffect } from 'react';

3.5 仅类型导入(TypeScript 3.8+)

import type { User, Product } from './types';
// 或
import { type User, type Product } from './types';

仅类型导入在编译后会被完全移除,不会产生运行时依赖。

4. 模块解析策略

TypeScript 提供了两种模块解析策略:

4.1 Classic(经典)

TypeScript 原始的解析策略,现在很少使用。

4.2 Node(Node.js)

与 Node.js 的模块解析行为一致,是默认策略。

import { a } from './module';
// 查找顺序:
// 1. ./module.ts
// 2. ./module.tsx
// 3. ./module.d.ts
// 4. ./module/package.json("types" 字段)
// 5. ./module/index.ts

import { b } from 'lodash';
// 查找顺序:
// 1. node_modules/lodash.ts
// 2. node_modules/lodash.tsx
// 3. node_modules/lodash.d.ts
// 4. node_modules/lodash/package.json("types" 字段)
// 5. node_modules/@types/lodash.d.ts
// 6. node_modules/lodash/index.ts

5. 模块格式与编译目标

TypeScript 可以编译为不同的模块格式:

{
  "compilerOptions": {
    "module": "ESNext",      // ES Module
    // "module": "CommonJS",   // CommonJS
    // "module": "AMD",        // AMD
    // "module": "UMD",        // UMD
    // "module": "System",     // SystemJS
    "target": "ES2020"
  }
}

6. 声明文件(.d.ts)

为无类型的 JavaScript 库提供类型定义:

// lodash.d.ts
declare module 'lodash' {
  export function chunk<T>(array: T[], size?: number): T[][];
  export function compact<T>(array: (T | null | undefined | false | 0 | "")[]): T[];
  export function debounce<T extends (...args: any[]) => any>(
    func: T,
    wait?: number,
    options?: { leading?: boolean; trailing?: boolean }
  ): T;
}

7. 全局声明

扩展全局对象或模块:

// 扩展全局 Window 对象
declare global {
  interface Window {
    myLib: any;
    API_BASE_URL: string;
  }
}

// 扩展现有模块
declare module 'vue' {
  export interface ComponentCustomProperties {
    $http: AxiosInstance;
  }
}

// 声明非代码模块
declare module '*.svg' {
  const content: string;
  export default content;
}

declare module '*.css' {
  const content: { [className: string]: string };
  export default content;
}

深入理解

ES Module vs CommonJS

特性 ES Module CommonJS
语法 import/export require/module.exports
加载时机 编译时静态分析 运行时动态加载
循环依赖 支持 部分支持
浏览器支持 原生支持 需要打包工具
Tree Shaking 支持 不支持

动态导入

// 静态导入(编译时确定)
import { add } from './math';

// 动态导入(运行时确定)
async function loadModule() {
  const math = await import('./math');
  console.log(math.add(1, 2));
}

// 条件导入
if (condition) {
  const { feature } = await import('./feature');
  feature.init();
}

路径别名

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';

最佳实践

  1. 优先使用 ES Module:现代浏览器和 Node.js 都支持
  2. 显式类型导入:使用 import type 区分类型和值的导入
  3. 避免循环依赖:重构代码结构,使用接口或事件解耦
  4. 路径别名:简化模块导入路径
  5. ** barrels(索引文件)**:组织模块导出
// components/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Select } from './Select';

// 使用
import { Button, Input, Select } from './components';

面试要点

  1. 模块定义:包含顶级 import/export 的文件
  2. 导出方式:命名导出、默认导出、类型导出
  3. 导入方式:命名导入、默认导入、命名空间导入、仅类型导入
  4. 模块解析:Node 策略,查找顺序
  5. 声明文件.d.ts 为 JS 库提供类型
  6. 全局扩展declare globaldeclare module
  7. ESM vs CJS:静态 vs 动态、编译时 vs 运行时
  8. 动态导入import() 函数,支持代码分割