Menu

Module trong TypeScript: import, export và import type

Mọi tệp TypeScript có import hoặc export ở cấp cao nhất đều là một module. Tìm hiểu named export và default export, import type và export type, cách thiết lập module quyết định đầu ra là ES module hay CommonJS, và vì sao node16 và nodenext đòi đuôi .js trong câu lệnh import.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Module trong TypeScript là một tệp có ít nhất một câu lệnh import hoặc export ở cấp cao nhất. Mọi thứ khai báo trong đó là riêng tư với tệp, trừ khi bạn export nó, và các tệp khác import những gì chúng cần. Cú pháp là cú pháp ES module của JavaScript cộng thêm vài dạng chỉ dành cho kiểu.

Named export và default export

Một dự án có nhiều tệp, nên phía import trông như thế này. Default export được import không cần dấu ngoặc nhọn và có thể đặt tên bất kỳ; named export nằm trong ngoặc nhọn và giữ nguyên tên, trừ khi bạn đổi tên bằng as.

// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js";            // everything, as one object

const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
DạngThứ được import
import { a, b } from "./m.js"các named export a và b
import x from "./m.js"default export, dưới tên x
import * as m from "./m.js"một namespace object chứa mọi export
import { a as b } from "./m.js"a, được đổi tên thành b trong tệp này
import type { T } from "./m.js"chỉ kiểu, bị xóa khỏi đầu ra
import "./setup.js"chạy tệp để lấy tác dụng phụ của nó
export { a } from "./m.js"export lại a mà không import nó
export * from "./m.js"export lại mọi named export

Export lại cho phép một tệp (thường là index.ts) gom API công khai của cả một thư mục. Hành vi module của JavaScript thuần, như live binding và cache module, được trình bày ở trang ES module.

import type và export type

Kiểu không tồn tại lúc chạy, nên một câu lệnh import chỉ dùng làm kiểu không có gì để nạp. import type nói rõ điều đó, và câu lệnh biến mất khỏi JavaScript đầu ra. Modifier type cũng dùng được cho từng tên riêng bên trong một câu lệnh import thông thường.

import type { User } from "./models.js";        // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives

export type { User };                            // re-export a type only
export type UserId = User["id"];

Không có từ khóa này, TypeScript vẫn xóa những tên mà nó thấy chỉ là kiểu. Có hai thiết lập khiến từ khóa trở thành bắt buộc:

  • verbatimModuleSyntax: true giữ lại mọi import không được đánh dấu type, nên một import kiểu không đánh dấu là lỗi TS1484: 'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
  • Chạy trực tiếp tệp .ts trong Node (type stripping) sẽ xóa chú thích kiểu nhưng không xem xét các tệp khác. Một import { Point } không đánh dấu vẫn ở lại trong code, và Node báo lỗi lúc chạy với SyntaxError: The requested module './math.ts' does not provide an export named 'Point'.

Viết type cho mọi import chỉ dùng kiểu thì chạy được trong cả hai trường hợp, nên đó là thói quen nên tập.

Cannot find module

Khi đường dẫn trong câu lệnh import không dẫn tới một tệp hay một package có kiểu, trình biên dịch dừng lại với lỗi TS2307. Chạy ví dụ này để xem:

Đầu ra là index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Các nguyên nhân thường gặp:

  • Gõ sai đường dẫn tương đối, hoặc thiếu ./ (không có nó, tên sẽ được tìm trong node_modules).
  • Một package JavaScript không kèm kiểu: cài @types/{package} nếu có, hoặc viết tệp khai báo cho nó.
  • Một subpath mà package không liệt kê trong trường exports của package.json. Với cách phân giải node16, nodenext và bundler, chỉ những entry point được liệt kê mới import được.

Đầu ra ES module hay CommonJS

Bạn luôn viết import và export. Tùy chọn module trong tsconfig.json quyết định JavaScript nào được sinh ra.

// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));

Với node16, node18, node20 hoặc nodenext, TypeScript làm theo chính quy tắc của Node cho từng tệp:

TệpĐịnh dạng đầu ra
.ts trong package có "type": "module"ES module
.ts trong package không có trường đóCommonJS
.mtsluôn là ES module, xuất ra .mjs
.ctsluôn là CommonJS, xuất ra .cjs

Với module: "esnext" hoặc "preserve", đầu ra giữ nguyên import/export, đúng thứ mà các bundler như Vite và esbuild mong đợi. Khác biệt lúc chạy giữa hai hệ thống module nằm ở trang CommonJS và ESM.

Đuôi tệp trong câu lệnh import

Với module: node16 hoặc nodenext, một tệp ES module phải ghi đúng tên tệp mà Node thực sự nạp, và đó là tệp .js đã biên dịch. TypeScript ánh xạ ./math.js ngược về math.ts để kiểm tra kiểu.

import { add } from "./math";    // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling

Nội dung lỗi là Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Các tệp CommonJS với cùng thiết lập có thể bỏ đuôi tệp, vì require tự thử các đuôi.

Hai thiết lập khác thay đổi quy tắc này:

  • moduleResolution: "bundler" (với module là esnext, preserve hoặc commonjs) chấp nhận ./math không có đuôi, vì bundler sẽ phân giải nó.
  • rewriteRelativeImportExtensions: true cho phép bạn viết ./math.ts, chính đường dẫn chạy được khi Node chạy trực tiếp tệp .ts, và viết lại nó thành ./math.js ở đầu ra.

Phân giải module và paths

Với một tên trần như "zod", TypeScript tìm trong node_modules, đọc package.json của package (các trường exports và types), và nếu không có thì tìm tới node_modules/@types/zod. Tên tương đối (./, ../) được phân giải từ tệp đang import. Tệp .json cũng import được; các thiết lập cần thiết nằm ở trang JSON.

paths trong tsconfig.json thêm bí danh cho các thư mục của chính bạn:

{
    "compilerOptions": {
        "module": "esnext",
        "moduleResolution": "bundler",
        "paths": {
            "@lib/*": ["./src/lib/*"]
        }
    }
}

paths chỉ ảnh hưởng tới việc kiểm tra kiểu. Tệp được sinh ra vẫn ghi import { v } from "@lib/util", nên phải có thứ khác phân giải nó lúc chạy: một bundler được cấu hình cùng bí danh, hoặc trường imports của Node trong package.json (bí danh bắt đầu bằng #, như "#lib/*": "./dist/lib/*"), cách này chạy được mà không cần công cụ nào thêm. baseUrl đã bị bỏ trong TypeScript 7 (lỗi TS5102); hãy viết các mục paths tương đối với tsconfig.json, bắt đầu bằng ./.

Câu hỏi thường gặp

import và import type trong TypeScript khác nhau thế nào?

import type { User } from "./user.js" chỉ đưa vào được kiểu, và cả câu lệnh bị xóa khỏi JavaScript đầu ra. Một import thông thường đưa vào được cả giá trị lẫn kiểu; TypeScript bỏ những tên chỉ được dùng như kiểu, nhưng với verbatimModuleSyntax nó bắt bạn đánh dấu chúng bằng type để đầu ra đúng như những gì bạn viết.

Vì sao TypeScript muốn có .js trong đường dẫn import?

Với module: node16 hoặc nodenext, một tệp ES module phải import bằng đúng tên tệp mà Node sẽ nạp lúc chạy, và tệp đó là .js đã biên dịch. Khi kiểm tra kiểu, TypeScript phân giải ./math.js thành math.ts. Bỏ đuôi tệp trong một tệp ES module là lỗi TS2835.

Nên dùng export default hay named export trong TypeScript?

Cả hai đều dùng được. Nhiều nhóm thích named export hơn: tên giống nhau ở mọi tệp import nó, trình soạn thảo tự import chúng một cách đáng tin cậy, và đổi tên là một thao tác refactor chứ không phải tìm kiếm thủ công. Default export cho phép mỗi nơi import tự chọn tên riêng.

Tùy chọn paths trong tsconfig có thay đổi câu lệnh import ở đầu ra không?

Không. paths chỉ cho trình kiểm tra kiểu biết nơi tìm một module. JavaScript được sinh ra giữ nguyên "@lib/util" như bạn viết, nên một bundler, hoặc trường imports của Node trong package.json, phải phân giải nó lúc chạy.

Làm sao để sửa lỗi "Cannot find module" trong TypeScript?

Lỗi TS2307 nghĩa là đường dẫn không trỏ tới tệp nào TypeScript tìm thấy, hoặc package không kèm kiểu. Hãy kiểm tra đường dẫn tương đối và đuôi tệp, cài package @types/... của thư viện nếu nó không có kiểu sẵn, hoặc viết một tệp khai báo nhỏ cho nó.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu