الوحدة في TypeScript ملف فيه import أو export واحد على الأقل على المستوى الأعلى. كل ما يُعرّف فيه خاص بالملف ما لم تصدّره بـ export، والملفات الأخرى تستورد بـ import ما تحتاجه. الصياغة هي صياغة وحدات ES في JavaScript مع بعض الأشكال الخاصة بالأنواع.
التصدير المسمى والتصدير الافتراضي
في المشروع ملفات كثيرة، وجانب الاستيراد يبدو هكذا. يُستورد التصدير الافتراضي دون أقواس معقوصة ويمكن أن يأخذ أي اسم؛ أما التصديرات المسماة فتوضع بين أقواس معقوصة وتحتفظ بأسمائها ما لم تعد تسميتها بـ 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" | التصديرين المسميين a وb |
import x from "./m.js" | التصدير الافتراضي، باسم x |
import * as m from "./m.js" | كائن namespace يضم كل التصديرات |
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" | يعيد تصدير كل التصديرات المسماة |
تتيح إعادة التصدير لملف واحد (غالبًا index.ts) أن يجمع الواجهة العامة لمجلد. أما سلوك الوحدات في JavaScript العادية، مثل الروابط الحية والتخزين المؤقت للوحدات، فمشروح في وحدات ES.
import type وexport type
الأنواع غير موجودة وقت التشغيل، لذلك لا يوجد ما يُحمّل من استيراد يُستخدم كنوع فقط. تقول import type ذلك صراحة، وتختفي العبارة من ناتج JavaScript. ويعمل المعدِّل type أيضًا على اسم واحد داخل استيراد عادي.
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يُبقي كل استيراد غير مميز بـtype، لذلك يكون استيراد النوع غير المميز الخطأ 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 في كل استيراد للأنواع فقط تعمل في الحالتين، لذلك هي العادة التي ينبغي اكتسابها.
Cannot Find Module
عندما لا يقود المسار في الاستيراد إلى ملف أو حزمة فيها أنواع، يتوقف المترجم مع الخطأ 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 أو 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 |
.ts في حزمة ليس فيها ذلك | CommonJS |
.mts | وحدة ES دائمًا، ويُخرج كـ .mjs |
.cts | CommonJS دائمًا، ويُخرج كـ .cjs |
مع module: "esnext" أو "preserve" يحتفظ الناتج بـ import/export، وهو ما تتوقعه أدوات التجميع مثل Vite وesbuild. الفروق بين نظامي الوحدات وقت التشغيل موجودة في CommonJS مقابل ESM.
امتدادات الملفات في الاستيراد
مع module: node16 أو nodenext يجب أن يسمّي ملف وحدة ES الملف الذي ستحمّله 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دون امتداد، لأن أداة التجميع تحلّه.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"، لذلك يجب أن يحلّه شيء آخر وقت التشغيل: أداة تجميع مضبوطة على الاسم البديل نفسه، أو الحقل 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 في مسارات الاستيراد؟
مع module: node16 أو nodenext يجب أن يستورد ملف وحدة ES باسم الملف الحقيقي الذي ستحمّله Node وقت التشغيل، وذلك الملف هو .js المُترجم. يربط TypeScript المسار ./math.js بالملف math.ts أثناء فحص الأنواع. وحذف الامتداد في ملف وحدة ES هو الخطأ TS2835.
هل أستخدم export default أم التصدير المسمى في TypeScript؟
كلاهما يعمل. كثير من الفرق تفضّل التصدير المسمى: الاسم نفسه في كل ملف يستورده، والمحررات تستورده تلقائيًا بشكل موثوق، وإعادة التسمية تصبح عملية refactor لا بحثًا. أما التصدير الافتراضي فيتيح لكل مستورد أن يختار الاسم الذي يريده.
هل يغيّر الخيار paths في tsconfig عبارة import في الناتج؟
لا. يخبر paths مدقق الأنواع فقط بمكان الوحدة. يحتفظ JavaScript الناتج بـ "@lib/util" كما هو مكتوب، لذلك يجب أن تحلّه أداة تجميع، أو الحقل imports الخاص بـ Node في package.json، وقت التشغيل.
كيف أصلح الخطأ "Cannot find module" في TypeScript؟
الخطأ TS2307 يعني أن المسار لا يشير إلى ملف يستطيع TypeScript إيجاده، أو أن حزمة ما لا تأتي بأنواع. تحقق من المسار النسبي والامتداد، وثبّت حزمة @types/... الخاصة بالحزمة إذا لم تكن فيها أنواع مدمجة، أو اكتب لها ملف تعريفات صغيرًا.