Menu

TypeScript Modules: import, export ו-import type

כל קובץ TypeScript עם import או export ברמה העליונה הוא מודול. כאן תלמדו named exports ו-default exports, את import type ו-export type, איך ההגדרה module מחליטה בין פלט ES module ל-CommonJS, ולמה node16 ו-nodenext דורשים סיומת .js ב-imports.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

מודול ב-TypeScript הוא קובץ עם לפחות import או export אחד ברמה העליונה. כל מה שמוצהר בו פרטי לקובץ אלא אם עושים לו export, וקבצים אחרים עושים import למה שהם צריכים. התחביר הוא תחביר ה-ES modules של JavaScript, ועוד כמה צורות שמיועדות לטיפוסים בלבד.

Named exports ו-default exports

בפרויקט יש קבצים רבים, אז הצד המייבא נראה כך. ה-default export מיובא בלי סוגריים מסולסלים ויכול לקבל כל שם; named exports נכתבים בתוך סוגריים מסולסלים ושומרים על השמות שלהם אלא אם משנים אותם עם 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);
צורהמה היא מייבאת
import { a, b } from "./m.js"את ה-named exports a ו-b
import x from "./m.js"את ה-default export, בשם x
import * as m from "./m.js"אובייקט namespace שמכיל כל export
import { a as b } from "./m.js"את a, בשם b בקובץ הזה
import type { T } from "./m.js"טיפוסים בלבד, מוסר מהפלט
import "./setup.js"מריץ את הקובץ בשביל תופעות הלוואי שלו
export { a } from "./m.js"מייצא מחדש את a בלי לייבא אותו
export * from "./m.js"מייצא מחדש כל named export

ייצוא מחדש מאפשר לקובץ אחד (לרוב index.ts) לאסוף את ה-API הציבורי של תיקייה. התנהגות מודולים של JavaScript רגיל, כמו live bindings ו-caching של מודולים, מוסברת ב-ES modules.

import type ו-export type

טיפוסים לא קיימים בזמן ריצה, ולכן ל-import שמשמש רק כטיפוס אין מה לטעון. import type אומר זאת במפורש, וההצהרה נעלמת מפלט ה-JavaScript. המודיפייר type עובד גם על שם בודד בתוך import רגיל.

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"];

בלי מילת המפתח, TypeScript עדיין מסיר שמות שהוא רואה שהם רק טיפוסים. שתי הגדרות הופכות את מילת המפתח לחובה:

  • verbatimModuleSyntax: true שומר כל import שלא מסומן ב-type, ולכן import של טיפוס שלא סומן הוא שגיאה TS1484: 'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
  • הרצה ישירה של קבצי .ts ב-Node (type stripping) מסירה הגדרות טיפוס אבל לא מסתכלת על קבצים אחרים. import { Point } לא מסומן נשאר בקוד, ו-Node נכשל בזמן ריצה עם SyntaxError: The requested module './math.ts' does not provide an export named 'Point'.

כתיבת type על כל import שמיועד לטיפוסים בלבד עובדת בשני המקרים, ולכן זה ההרגל שכדאי לפתח.

Cannot Find Module

כשהנתיב ב-import לא מוביל לקובץ או לחבילה עם טיפוסים, הקומפיילר נעצר עם שגיאה TS2307. הריצו את זה כדי לראות:

הפלט הוא index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. הסיבות הנפוצות:

  • שגיאת כתיב בנתיב יחסי, או ./ חסר (בלעדיו, השם מחופש ב-node_modules).
  • חבילת JavaScript בלי טיפוסים מצורפים: התקינו את @types/{package} אם היא קיימת, או כתבו לה קובץ הצהרות.
  • נתיב משנה שהחבילה לא מפרטת בשדה exports של ה-package.json שלה. תחת פענוח node16, nodenext ו-bundler, אפשר לייבא רק נקודות כניסה שמפורטות שם.

פלט ES modules או CommonJS

תמיד כותבים import ו-export. האפשרות module ב-tsconfig.json קובעת איזה JavaScript יוצא.

// 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));

עם node16, node18, node20 או nodenext, TypeScript עוקב אחרי הכללים של Node עצמו לכל קובץ:

קובץפורמט הפלט
.ts בחבילה עם "type": "module"ES module
.ts בחבילה בלי זהCommonJS
.mtsתמיד ES module, נוצר כ-.mjs
.ctsתמיד CommonJS, נוצר כ-.cjs

עם module: "esnext" או "preserve", הפלט שומר על import/export, וזה מה ש-bundlers כמו Vite ו-esbuild מצפים לו. ההבדלים בין שתי מערכות המודולים בזמן ריצה מוסברים ב-CommonJS vs ESM.

סיומות קבצים ב-imports

תחת module: node16 או nodenext, קובץ ES module חייב לציין את הקובץ ש-Node באמת יטען, וזה קובץ ה-.js המקומפל. TypeScript ממפה את ./math.js חזרה ל-math.ts לצורך בדיקת הטיפוסים.

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

טקסט השגיאה הוא Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? קבצי CommonJS תחת אותן הגדרות יכולים להשמיט את הסיומת, כי require מנסה סיומות בעצמו.

שתי תצורות אחרות משנות את הכלל:

  • moduleResolution: "bundler" (עם module שמוגדר כ-esnext, preserve או commonjs) מקבל את ./math בלי סיומת, כי ה-bundler מפענח אותו.
  • rewriteRelativeImportExtensions: true מאפשר לכתוב ./math.ts, אותו נתיב שעובד כש-Node מריץ את קובץ ה-.ts ישירות, ומשכתב אותו ל-./math.js בפלט.

פענוח מודולים ו-paths

עבור שם חשוף כמו "zod", TypeScript מחפש ב-node_modules, קורא את ה-package.json של החבילה (השדות exports ו-types שלו), ואם לא מצא, עובר ל-node_modules/@types/zod. שמות יחסיים (./, ../) מפוענחים ביחס לקובץ המייבא. אפשר לייבא גם קובץ .json; ההגדרות שזה דורש מופיעות בדף JSON.

paths ב-tsconfig.json מוסיף כינויים לתיקיות שלכם:

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

paths משפיע רק על בדיקת הטיפוסים. הקובץ שנוצר עדיין אומר import { v } from "@lib/util", ולכן משהו אחר צריך לפענח אותו בזמן ריצה: bundler שמוגדר עם אותו כינוי, או השדה imports של Node ב-package.json (כינויים שמתחילים ב-#, כמו "#lib/*": "./dist/lib/*"), שעובד בלי שום כלי נוסף. baseUrl הוסר ב-TypeScript 7 (שגיאה TS5102); כתבו את רשומות ה-paths ביחס ל-tsconfig.json עם ./ בהתחלה.

שאלות נפוצות

מה ההבדל בין import ל-import type ב-TypeScript?

import type { User } from "./user.js" יכול להביא רק טיפוסים, וכל ההצהרה מוסרת מפלט ה-JavaScript. import רגיל יכול להביא ערכים וטיפוסים; TypeScript משמיט את השמות שמשמשים רק כטיפוסים, אבל עם verbatimModuleSyntax הוא דורש לסמן אותם ב-type כדי שהפלט יהיה בדיוק מה שכתבתם.

למה TypeScript רוצה .js בנתיבי import?

תחת module: node16 או nodenext, קובץ ES module חייב לבצע import עם שם הקובץ האמיתי ש-Node יטען בזמן ריצה, והקובץ הזה הוא ה-.js המקומפל. TypeScript מפענח את ./math.js ל-math.ts בזמן בדיקת הטיפוסים. השמטת הסיומת בקובץ ES module היא שגיאה TS2835.

האם להשתמש ב-export default או ב-named exports ב-TypeScript?

שניהם עובדים. צוותים רבים מעדיפים named exports: השם זהה בכל קובץ שמייבא אותו, עורכי קוד מוסיפים להם import אוטומטי באופן אמין, ושינוי שם הוא refactor ולא חיפוש. default export מאפשר לכל מייבא לבחור שם משלו.

האם האפשרות paths ב-tsconfig משנה את ה-import בפלט?

לא. paths רק אומר לבודק הטיפוסים איפה למצוא מודול. ה-JavaScript שנוצר משאיר את "@lib/util" כפי שנכתב, ולכן bundler, או השדה imports של Node ב-package.json, צריך לפענח אותו בזמן ריצה.

איך מתקנים את "Cannot find module" ב-TypeScript?

שגיאה TS2307 אומרת שהנתיב לא מוביל לקובץ ש-TypeScript מוצא, או שחבילה לא מגיעה עם טיפוסים. בדקו את הנתיב היחסי ואת הסיומת, התקינו את חבילת @types/... של החבילה אם אין לה טיפוסים מובנים, או כתבו לה קובץ הצהרות קטן.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל