09. ジェネリクス 11. 非同期とエラー処理 資料トップへ戻る
import type を使い分けられる// lib.ts
export const greet = (name: string) => `Hello, ${name}`;
export type User = { name: string };
// main.ts
import { greet, type User } from "./lib.ts";
型は実行時に消えるため、型だけを import している場合、その行ごと消えます。明示すると意図が伝わり、消えることも保証されます。
import type { User } from "./types.ts"; // 型だけ。実行時には消える
import { greet, type User } from "./lib.ts"; // 値と型を混ぜる書き方
import type を使う理由は、循環参照と副作用の回避です。
型だけが必要なのに通常の import を書くと、そのモジュールが実行時に読み込まれます。読み込むだけで副作用があるモジュールや、循環参照しているモジュールでは問題になります。型だけを使うなら import type と書いてください。
// 名前付き export(推奨)
export const greet = () => {};
import { greet } from "./lib.ts";
// default export
export default function () {}
import whateverName from "./lib.ts"; // 名前を自由に付けられてしまう
| 項目 | 名前付き | default |
|---|---|---|
| 名前の統一 | 強制される | 自由に付けられる |
| リネームの追従 | エディタが追える | 追えない |
| CJS との相互運用 | 問題が少ない | 食い違いが起きる(10.2) |
| 方式 | 読み込み | 公開 | 由来 |
|---|---|---|---|
| ESM | import | export | JavaScript の標準。現在の主流 |
| CJS | require() | module.exports | Node.js が独自に導入したもの |
{
"type": "module" // ← ESM。省略すると CJS
}
package.json の "type" が ESM と CJS を切り替える// main-dirname.ts
console.log(typeof __dirname === "undefined" ? "__dirname は使えない" : "__dirname は使える");
console.log(typeof require === "undefined" ? "require は使えない" : "require は使える");
$ node main-dirname.ts # package.json に type がない場合
__dirname は使える
require は使える
$ node main-dirname.ts # "type": "module" の場合
__dirname は使えない
require は使えない
ESM では __dirname・__filename・require が存在しません。
代わりに import.meta.dirname(Node 20.11 以降)を使います。
const dir = import.meta.dirname; // ESM でのファイルの場所
| エラー | 原因 | 対処 |
|---|---|---|
ERR_REQUIRE_ESM | CJS から ESM を require した | 呼ぶ側を ESM にする("type": "module") |
Cannot use import statement outside a module | CJS のファイルで import を書いた | 同上 |
ERR_MODULE_NOT_FOUND | ESM 拡張子を省略した | "./lib.js" のように拡張子を書く |
__dirname is not defined | ESM で __dirname を使った | import.meta.dirname にする |
x is not a function(実行時) | default export の食い違い | 下記参照 |
ESM ESM では拡張子の省略ができません。しかも書くのは出力後の拡張子です。
import { greet } from "./lib"; // ESM では動かない
import { greet } from "./lib.js"; // ← .ts を書いても .js と書く(出力後の名前)
import { greet } from "./lib.ts"; // Node で直接 .ts を実行する場合はこちら
ここが混乱の元です。
tsc でコンパイルして実行する場合、lib.ts は lib.js になります。import のパスは書き換えられないため、最初から .js と書く必要があります。
一方、Node の型ストリップや tsx で .ts を直接実行する場合は .ts と書きます。どちらの運用にするかを最初に決めてください。混ぜると必ず事故ります。
// CJS のライブラリ
module.exports = function foo() {};
// ESM から使う
import foo from "some-cjs-lib"; // 動く場合と動かない場合がある
import * as foo from "some-cjs-lib"; // foo.default になることがある
ESMCJS module.exports と export default は別の仕組みです。Node とバンドラで解釈が異なることもあります。
{
"compilerOptions": {
"esModuleInterop": true // 相互運用を助ける(既定で有効なことが多い)
}
}
「型は通るのに実行時に x is not a function になる」ときは、まずこの食い違いを疑ってください。
// lib.d.ts — 実装はなく、形だけを宣言する
export declare function greet(name: string): string;
export declare const VERSION: string;
tsc は .ts をコンパイルするとき、declaration: true を指定すると .d.ts も出力します。ライブラリを配布するときは、.js と .d.ts をセットで配ります。
| 場所 | 確認方法 |
|---|---|
| ライブラリ同梱 | package.json の "types" または "typings" フィールド |
@types/* | node_modules/@types/ 配下 |
| 自分で書いた | プロジェクト内の .d.ts |
TS7 02. 実行環境で触れたとおり、@types/* を入れるだけでは読み込まれません。
{
"compilerOptions": {
"types": ["node"] // ← 使うものを列挙する
}
}
types を書くと、書いたものだけが読まれます。
@types/node と @types/jest の両方を使うなら ["node", "jest"] と書きます。1つだけ書くと、もう一方が読まれなくなります。
$ npm i -D @types/ライブラリ名
見つからなければ、次の3つから選びます。
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function doSomething(input: string): number;
}
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src/**/*.ts", "types/**/*.d.ts"]
}
使う関数だけ書けば十分です。全体を網羅する必要はありません。
// types/legacy-lib.d.ts
declare module "legacy-lib"; // すべて any になる
これは any の伝播を招きます。
そのライブラリから来る値がすべて any になり、下流の検査が無効化されます(06)。使うなら受け口で unknown に変換してください。
import { doSomething } from "legacy-lib";
// any のまま使わず、受け口で unknown にする
const result: unknown = doSomething("x");
if (typeof result === "number") {
console.log(result.toFixed(2));
}
実務で最も勧められる形です。型のない世界を1ファイルに閉じ込めます。
// src/legacy-wrapper.ts — ここだけが型のない世界に触れる
import * as legacy from "legacy-lib";
export function doSomething(input: string): number {
const r: unknown = (legacy as { doSomething(s: string): unknown }).doSomething(input);
if (typeof r !== "number") {
throw new Error(`legacy-lib が想定外の値を返しました: ${String(r)}`);
}
return r;
}
他のファイルは legacy-wrapper.ts だけを使います。ライブラリを差し替えるときも、直すのはこのファイルだけです。
| 手段 | 手間 | 安全性 | 使いどころ |
|---|---|---|---|
@types を入れる | 小 | 高 | あるなら必ずこれ |
最小の .d.ts | 中 | 中 | 使う範囲が狭いとき |
declare module だけ | 小 | 低 | 暫定。受け口で unknown にする |
| ラッパー | 中 | 高 | 実務で最も勧められる |
まとめ
| 項目 | この章の結論 |
|---|---|
import type | 型だけなら明示する。循環参照と副作用を避けられる |
| ESM / CJS | package.json の "type" が決める。ESM では __dirname・require が使えない |
| 拡張子 | ESM は省略できない。tsc 経由なら .js、直接実行なら .ts。運用を先に決める |
@types | TypeScript 7 では types に列挙する。書いたものだけが読まれる |
| 型のないライブラリ | ラッパーを1枚かぶせる。型のない世界を1ファイルに閉じ込める |
次は 11. 非同期とエラー処理 です。Promise の型と、catch が unknown である理由を扱います。