Note
2025年6月14日追記: unplugin-typiaはアーカイブされました。
詳細に関してはREADME.mdを参照してください。
TL;DR
- この度、
unplugin-typiaという Library を作りました unplugin-typiaを使うと今までめんどくさかったTypiaの導入が簡単になりますVite、esbuild、webpackなどフロントエンドで主流の様々なbundlerに対応していますNext.jsでも簡単に使えますBunにも対応しています
https://github.com/ryoppippi/unplugin-typia/ https://jsr.io/@ryoppippi/unplugin-typia https://typia.io/docs/setup/#unplugin-typia
はじめに
皆さんはTypeScriptでのValidationにはどのような Library を使っていますか?
zodはエコシステムが硬いし、最近だとvalibotが流行りつつありますね。
またarktypeも注目に値するLibraryです。 typeboxも耳にする機会が増えてきました。
また個人的には(厳密にはValidatorではないですが)、unknownutilも手に馴染んでよく使っています。
既存のValidation Library/TypeScriptに足りないもの
TypeScriptの型システムは非常に強力です。 アップデートを重ねた結果とても豊かな表現力を持ち、型システムとしてチューリング完全であることが知られています^https://github.com/Microsoft/TypeScript/issues/14833。 型パズルを駆使すればbrainf**k interpreter^https://github.com/susisu/typefuckでさえ書けてしまいます。
しかし、型システムは実行時には消えてしまいます。また、TypeScriptでは原則として「値から型を作る」ことはできますが、「型から値を作る」ことはできません。
型から値への変換の限界
TypeScriptの型情報は開発時の安全性を提供しますが、実行時に動作することはありません。 例えば、以下のようなコードを考えます:
function someFunction(): any {}
const value: string = someFunction();
このコードではsomeFunctionの戻り値がstring型であることを保証するためにtype assertionや明示的なValidationが必要ですが、型自体からそのようなValidation logicを生成することはできません。
zodやvalibotなどの既存のValidation Libraryは、TSの Library として用意されたDSL(ドメイン固有言語)で assertion を定義し、そこからTypeScriptの型を生成(推論)することで型安全を担保しています。
これでは既存の型に対する Validation 関数を素朴な方法で用意できず、DSLを覚えて、一から Validation 関数を作りなおす必要があります。
このため型とValidation Logicが分離してしまうことがあります。
また、TSという型システムがあるのに、さらにDSLで型システムに準じるものを作り直すのは直感的ではないですよね。
Typia とは
Typiaは、このような課題を解決するためのツールです。
- 高速:
Typiaは既存のValidation Library に比べて非常に高速です。「zodの1500倍速い」とも言われています^https://zenn.dev/hr20k_/articles/3ecde4239668b2#速度の比較。 - 型情報から Validation を生成: TypeScriptの型情報を元に Validation を生成します。コンパイルが必要ですが、これにより型チェックの正確性とパフォーマンスが向上します。
- シンプルな記法: 特定の Library 特有の記法を覚える必要はなく、TypeScriptの標準的な型記述から Validation を生成します。
- 多機能: Validation だけでなく、高速なJSON変換、JSON Schema生成、ProtoBuf生成、ランダムデータ生成などの機能も提供します。
Typiaの高速さは特筆すべき点であり、実際に使用したプロジェクトでは、APIドキュメントから自動生成された大量のTypeScript型ファイルを使って Validation を行う場面で非常に有効でした。
しかし、Typiaの真の強みは、「独自の記法を覚える必要がないこと」 にあります。
この点は既存の Validation Library との大きな差別化要因であり、Typiaの導入障壁を大幅に下げています。 Typiaを使えば、TypeScriptの標準的な記述に従うだけで、自然にValidationや他の機能を実現できるため、新しいLibraryに合わせてルールやシンタックスを覚える必要がありません。
開発者は既存のTypeScriptの知識だけでTypiaを使いこなすことができるのです。
Typiaは実行時に消え去る運命に合った型情報に息を吹き込む Library と言えるでしょう。
Typia のコードを見てみよう
シンプルな例
手始めに簡単な例から:
import typia from 'typia';
const b = typia.is<string>('hello world');
console.log(b);
このコードは、'hello world'がstring型であるかどうかをチェックするコードです。
これをTypiaでコンパイルすると以下のようなコードが生成されます。
// 生成されたコード
import typia from 'typia';
const b = ((input: any): input is string => {
return typeof input === 'string';
})('hello world');
console.log(b); // true
このように、typia.isは 型情報から Validation関数を生成します。
生成されたコードを見てみると、importされているtypiaはどこからも参照されていないので、このあとbundlerを挟むとtree-shakingされることがわかります。
Object型の例
次は、一般的な型に対してValidationを行うコードを見てみましょう。
例えば、以下のようなMember型があり、それをチェックするコードがあるとします。
ここではValidationを行う関数を生成するためにtypia.isを使っています。
// 元のコード
import typia from 'typia';
type Member = {
/**
* @format uuid
*/
id: string;
/**
* @type uint32
* @minimum 20
* @exclusiveMaximum 100
*/
age: number;
name: string;
time?: Date;
};
const member = { id: '', name: 'taro', age: 20 } as const satisfies Member;
console.log(typia.is<Member>(member)); // false
これをTypiaを使ってコンパイルするとランタイムでの型チェックを行うコードが生成されます。
少し長いので折りたたんでいます。
生成されたコード
// Typiaによって生成されたコード
import typia from 'typia';
// ←もはや`typia`は使用されてないのでtree-shakingの対象になる
type Member = {
/**
* @format uuid
*/
id: string;
/**
* @type uint32
* @minimum 20
* @exclusiveMaximum 100
*/
age: number;
name: string;
time?: Date;
};
const member = { id: '', name: 'taro', age: 20 } as const satisfies Member;
console.log(
((input: any): input is Member => {
const $io0 = (input: any): boolean =>
typeof input.id === 'string'
&& /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(
input.id,
)
&& typeof input.age === 'number'
&& Math.floor(input.age) === input.age
&& input.age >= 0
&& input.age
/**
* @format uuid
*/ <= 4294967295
&& input.age >= 20
&& input.age < 100
&& typeof input.name === 'string'
&& (undefined === input.time || input.time instanceof Date);
return typeof input === 'object' && input !== null && $io0(input);
})(member),
); // false
またtypiaにはJSDocの代わりにtagを用いる別の書き方もあります。
生成されるコードは同じですが、Editor上で補完が効くので便利です。
import typia, { tags } from 'typia';
type Member = {
id: string & tags.Format<'uuid'>;
name: string;
time?: Date;
age: number
& tags.Type<'uint32'>
& tags.Minimum<20>
& tags.ExclusiveMaximum<100>;
};
const member = { id: '', name: 'taro', age: 20 } as const satisfies Member;
console.log(typia.is<Member>(member)); // false
型関数を用いたより複雑な型に対してもValidation関数を生成できます。
比較的複雑な型の例
// 元のコード
import typia from 'typia';
type D = {
/**
* @format uuid
*/
id: string;
age?: number | null;
};
type Member = {
name: string;
id: string;
details: D;
};
type ValidateType = Pick<Member, 'details'> & Omit<Member, 'id'>;
console.log(typia.is<ValidateType>({} as unknown)); // false
// 生成されたコード
type D = {
/**
* @format uuid
*/
id: string;
age?: number | null;
};
type Member = {
name: string;
id: string;
details: D;
};
type ValidateType = Pick<Member, 'details'> & Omit<Member, 'id'>;
console.log(
((input: any): input is ValidateType => {
const $io0 = (input: any): boolean =>
typeof input.details === 'object'
&& input.details !== null
&& $io1(input.details)
&& typeof input.name === 'string';
const $io1 = (input: any): boolean =>
typeof input.id === 'string'
&& /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(
input.id,
)
&& (input.age === null
|| undefined === input.age
|| typeof input.age === 'number');
return typeof input === 'object' && input !== null && $io0(input);
})({} as unknown),
); // false
TypiaのDocumentにはPlaygroundが用意されているので、実際に試してみるといいでしょう。 https://typia.io/playground/
詳細なベンチマーク結果はこちらの記事を参照してください。 https://zenn.dev/hr20k_/articles/3ecde4239668b2
Typiaの導入の課題
これまでTypiaを使おうとすると、いくつかのハードルが必要でした。
Typiaには2つのモードがあります。TransformationモードとGenerationモードです。
- Transformationモード:
tscのTransform APIを使って型情報からValidationを生成するモード。tsc実行時にValidationのコードが生成される。 - Generationモード:
TypiaのCLIを使って型情報からValidationを生成するモード。Bundlerがtscを使わない場合に使う。
癖がなくハマりずらいのはGenerationモードです。 TypiaのCLIを使ってコードを生成し、それを他のファイルからimportするだけで使うことができます。
しかし、ビルドステップが一つ増えますし、管理するコードも増えるのでできればTransformationモードを使いたいですよね。
ところがTransformationモードは導入のハードルが高いです。
直接tscコマンドを叩いてコンパイルする場合は導入が簡単ですが、他のBundlerを使う場合にはあらかじめtscを経由してBundleするように設定をする必要があります。
viteの例
import react from '@vitejs/plugin-react';
import typescript from 'rollup-plugin-typescript2';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
esbuild: false,
plugins: [
react(),
typescript(),
],
});
ただ手元だとうまく動かないことも多かったです。特にts/tsx以外のhtml-ishな言語^https://biomejs.dev/blog/biome-v1-6/#partial-support-for-astro-svelte-and-vue-files、例えばsvelteやvueなどを使用しているプロジェクトでは特に問題が多かったです。
https://github.com/samchon/typia/issues/812
そのため Webpack などのBundlerを用いたプロジェクト( Next.js など )では、tscを使ってコンパイルをしていないため、Generationモードを使う必要がありました。
Generationモードを使ってもいいのですが、まあビルドステップが一つ増えますし、管理するコードも増えるので、できればTransformationモードを使いたいですよね。
unplugin-typia
そこで、私はunplugin-typia を作りました。
https://github.com/ryoppippi/unplugin-typia https://jsr.io/@ryoppippi/unplugin-typia
unplugin-typia は unplugin とtscを組み合わせて作っています。 unplugin とは、Vite や esbuild、webpack などの複数のbundlerに対応したプラグインを共通のAPIで作るためのLibraryです。
unplugin-typiaを使うと、複雑な設定をすることなく、bundlerでtypiaのTransform モード相当の体験を得ることができます。
Note
それぞれのbundlerの詳しい説明はJSR上のdocを参照してください。 またexamplesには各bundlerごとのサンプルがあります。 さらにいくつかのサンプルについては実際にデプロイされているので、試してみてください。
| bundler | deploy site | GitHub |
|---|---|---|
| Vite | Cloudflare pages | examples/vite-react |
| Vite + Sveltekit | Cloudflare pages | examples/sveltekit |
| Vite + Hono | Cloudflare pages | examples/vite-hono |
| Next.js | Vercel | examples/nextjs |
(Vite + HonoはAPIのみのデプロイです。README.mdを読んでcurlしてね!)
実際にunplugin-typiaを使ってみよう
では簡単にunplugin-typiaとTypiaを使って遊んでみましょう。
Bun と一緒に使ってみる
一番手っ取り早く使う方法はBun.buildを使うことです。
Github上にテンプレートを作成したので、それを使ってプロジェクトを作成します。
https://github.com/ryoppippi/bun-typia-template
git clone https://github.com/ryoppippi/bun-typia-template
cd bun-typia-template
bun i
# 以下のコマンドで実行
bun run index.ts
# もしビルドして実行したい場合
bun run build # build.tsを実行してビルドを行う。`./out`にビルドされたファイルが出力される
bun run ./out/index.js
# もしくは
node ./out/index.js
これだけで、Typiaを使ったコードを実行することができます。
とっても簡単ですね。
Vite + Hono + unplugin-typiaで使ってみる
次に、ViteとHonoとunplugin-typiaを使って遊んでみましょう。
プロジェクトの作成
まずは、プロジェクトを作成します。
npm create hono@latest ./my-app -- --template cloudflare-pages
cd my-app
npm install
次に、unplugin-typiaをインストールします。 unplugin-typiaはJSRに公開されているので、jsr コマンドを使ってインストールします。
npx jsr add -D @ryoppippi/unplugin-typia
そしてtypiaを導入します。Typiaのドキュメントを参考にしてください。
npm i typia # typiaをインストール
npx typia setup # typiaのsetup wizardを実行
npm i @hono/typia-validator --force # honoのtypia-validatorをインストール
これで準備ができましたね!
unplugin-typiaの設定をする
unplugin-typiaはviteのプラグインとして使います。 vite.config.tsに以下のように設定します。
import build from '@hono/vite-cloudflare-pages'
import devServer from '@hono/vite-dev-server'
import adapter from '@hono/vite-dev-server/cloudflare'
import { defineConfig } from 'vite'
+import UnpluginTypia from '@ryoppippi/unplugin-typia/vite';
export default defineConfig({
plugins: [
build(),
+ UnpluginTypia(), // unplugin-typiaを追加
devServer({
adapter,
entry: 'src/index.tsx'
})
]
})
Typiaを使って実装してみる
では、src/index.tsxに以下のコードを書いてみましょう。
import { Hono } from 'hono'
import { renderer } from './renderer'
+import typia from 'typia'
+import { typiaValidator } from '@hono/typia-validator'
+interface Props {
+ name: string
+}
const app = new Hono()
app.use(renderer)
app.get('/', (c) => {
return c.render(<h1>Hello!</h1>)
})
+app.post('/',
+ typiaValidator('json', typia.createValidate<Props>()),
+ (c) => {
+ const data = c.req.valid('json');
+
+ return c.json({
+ success: true,
+ message: `Hello ${data.name}!`
+ })
+ }
+)
export default app
これで準備ができました。
実行してみる
それでは、実行してみましょう。
$ npm run dev
> dev
> vite
╭──────────────────────────────────╮
│ │
│ [unplugin-typia] Cache enabled │
│ │
╰──────────────────────────────────╯
(!) Could not auto-determine entry point from rollupOptions or html files and there are no explicit optimizeDeps.include patterns. Skipping dependency pre-bundling.
VITE v5.2.13 ready in 588 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
無事、起動できましたね!
それでは、APIを叩いてみましょう。
curlを使ってもいいのですが記述が長くなるので、ここではxhを使ってみましょう。
$ xh :5173 name=ryoppippi
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 45
Content-Type: application/json; charset=UTF-8
Date: Wed, 12 Jun 2024 11:14:19 GMT
Keep-Alive: timeout=5
{
"success": true,
"message": "Hello ryoppippi!"
}
無事、Validationが通り、APIが叩けましたね!
試しにnameを文字列ではなく数値で送ってみましょう。
$ xh :5173 name:=5
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 80
Content-Type: application/json; charset=UTF-8
Date: Wed, 12 Jun 2024 11:15:58 GMT
Keep-Alive: timeout=5
{
"success": false,
"error": [
{
"path": "$input.name",
"expected": "string",
"value": 5
}
]
}
Validation Error が返ってきましたね!
Cloudflare Pagesにデプロイしてみる
最後に、Cloudflare Pagesにデプロイしてみましょう。
$ npm run deploy
$ $npm_execpath run build && wrangler pages deploy
$ vite build
╭──────────────────────────────────╮
│ │
│ [unplugin-typia] Cache enabled │
│ │
╰──────────────────────────────────╯
vite v5.2.13 building SSR bundle for production...
✓ 50 modules transformed.
dist/_worker.js 67.14 kB
✓ built in 167ms The project you specified does not exist: "hono-vite". Would you like to create it?"
❯ Create a new project
✔ Enter the production branch name: … main
✨ Successfully created the 'hono-vite' project.
🌏 Uploading... (1/1)
✨ Success! Uploaded 1 files (1.61 sec)
✨ Compiled Worker successfully
✨ Uploading Worker bundle
✨ Uploading _routes.json
🌎 Deploying...
✨ Deployment complete! Take a peek over at https://xxx.pages.dev
無事デプロイできましたね! あとはこのURLを実際に呼んでみましょう。
$ xh https://xxx.pages.dev name=ryoppippi
# 省略
{
"success": true,
"message": "Hello ryoppippi!"
}
無事デプロイされたAPIが叩けましたね!
まとめ
unplugin-typiaを使うとTypiaの導入が簡単になりますVite、esbuild、webpackなどのbundlerに対応していますTypiaは楽しいTypiaは面白い!
ぜひTypiaを試してみてください!
Appendix
jsonup + typia
jsonupはTypeScriptで書かれたJSON Parserです。
JSON形式のLiteral Stringを与えると型推論を行うという、謎技術 Library です(本人曰くネタで作ったそうですが、TypeScriptのCompilerの限界に挑戦してる感が好きです)。
https://github.com/tani/jsonup
そして、jsonupとtypiaを組み合わせるとなんと、文字列からvalidation関数が生成されるという、なんとも不思議なことができます。
https://x.com/ryoppippi/status/1800183030235255019
typiaにはrandom generatorがついているので、JSONの文字列を例として与えるとそれに合致するランダムな値を生成する、なんてこともできます。
import type { ObjectLike } from 'jsonup';
import typia from 'typia';
const jsonSample = `{ "name": "jsonup", "age": 34}`;
/**
* type Obj = {
* name: string;
* age: number;
* }
*/
type Obj = ObjectLike<typeof jsonSample>;
console.log(typia.random<Obj>());
$ bun run ./index.ts
{ name: 'dvdnp', age: 49.25475568792122 }
$ bun run ./index.ts
{ name: 'htgywkaq', age: 13.818270173692525 }
$ bun run ./index.ts
{ name: 'pyurujgkvd', age: 47.19975642889989 }
https://github.com/ryoppippi/bun-typia-jsonup-experiments
GitHubからコードを落とせるのでぜひ遊んでみてください。
自分はjsonupやtype-festのような型パズルの Library が大好きなので、これらに息を吹き込めるようなtypiaはとても楽しいです。
typefuck + typia
typefuck とは、型レベルで実装されたBrainfuck interpreterです。
https://github.com/susisu/typefuck
この実装により、TypeScriptの型システムはチューリング完全であることがわかるのですが、こちらもTypiaと組み合わせることができます。
import type { Brainfuck } from '@susisu/typefuck';
import typia from 'typia';
type Program = '>, [>, I<[.<]';
type Input = 'Hello, world!';
type Output = Brainfuck<Program, Input>;
console.log(typia.is<Output>('!drow ,olleH')); // true
type-fest + typia
type-festとは、TypeScriptで型を操作する時の便利関数を集めたLibraryです。
https://github.com/sindresorhus/type-fest
たとえば、xor^https://ja.wikipedia.org/wiki/排他的論理和を実現するための型としてMergeExclusiveが用意されています。
これをTypiaと組み合わせると、それぞれのkeyに対して排他的なObject型ができあがります。
import type { MergeExclusive, SimplifyDeep } from 'type-fest';
import typia from 'typia';
type ExclusiveVariation1 = {
exclusive1: boolean;
};
type ExclusiveVariation2 = {
exclusive2: string;
};
type ExclusiveOptions = SimplifyDeep<
MergeExclusive<
ExclusiveVariation1,
ExclusiveVariation2
>
>;
const is = typia.createIs<ExclusiveOptions>();
console.log(is({ exclusive1: true })); // true
console.log(is({ exclusive2: 'string' })); // true
console.log(is({ exclusive1: true, exclusive2: 'string' })); // false
ExclusiveOptionsの型
type ExclusiveOptions = {
exclusive1?: undefined;
exclusive2: string;
} | {
exclusive2?: undefined;
exclusive1: boolean;
};
また、IntRangeという型もあります。この型は指定された範囲の整数を表します。
これを使って、1ケタの数字を表す型を作り、Validation関数を生成してみましょう。
さらに、random関数を使ってランダムな値を生成してみます。
import type { IntRange } from 'type-fest';
import typia from 'typia';
type Digit = IntRange<0, 10>; // 0 <= Digit < 10
const is = typia.createIs<Digit>();
console.log(is(5)); // true
console.log(is(11)); // false
console.log(is(-1)); // false
console.log(typia.random<Digit>()); // 5 (or any other number between 0 and 9)
random関数のコンパイル結果
console.log(((generator) => {
const $pick = typia.random.pick;
return $pick([
() => 0,
() => 1,
() => 2,
() => 3,
() => 4,
() => 5,
() => 6,
() => 7,
() => 8,
() => 9
])();
})());
自分の知る限り、従来型のValidation Libraryでこのようなロジックを組んだとしても、それが型(z.infer<foo>など)に反映されることはないと思います。
たとえばzodであれば
import { IntRange } from 'type-fest';
import { z } from 'zod';
const digit = z.number().int().min(0).max(10).transform(x => (x as IntRange<0, 10>));
とする必要がありますが、これは型とValidation Logicが分離してしまっていますよね。
Typiaは型情報からValidation Logicを生成するため、型とValidation Logicが一体となっているのが特徴です。
Note
Digitの例は、一応Typiaの記法を使うと
import {'\{'} tags {'\}'} from 'typia';
type Digit = number
& tags.Type<'uint32'>
& tags.Minimum<0>
& tags.ExclusiveMaximum<10>
と書くこともできます。
Typia記法を使うメリット
- 範囲で指定ができる(
type-festはunion型なので) - 範囲指定の方がJSON Schema等の生成に有利(
type-festだとunion型なのでとりうる数字が全て列挙される…)
type-fest記法を使うメリット
- Editor 上で補完が効く(取りうる値が個別の Literal 型として型情報に反映されているので)
- 範囲外の数値に対してEditor 上で型エラーが出る(取りうる値が型情報に反映されているので)
zod/valibotとバンドルサイズの比較
先ほどのTypiaでの実装例をzodとvalibotで書いた例と比較してみましょう。
import { z } from 'zod';
const Member = z.object({
id: z.string().uuid(),
age: z.number().int().min(20).max(99),
name: z.string(),
time: z.date().optional(),
});
type Member = z.infer<typeof Member>;
const member = { id: '', name: 'taro', age: 20 } as const satisfies Member;
console.log(Member.parse(member));
import * as v from 'valibot';
const Member = v.object({
id: v.pipe(v.string(), v.uuid()),
age: v.pipe(
v.number(),
v.integer(),
v.minValue(20),
v.maxValue(99) // exclusiveの代わりに-1しておく
),
name: v.string(),
time: v.optional(v.date()),
});
type Member = v.InferOutput<typeof Member>;
const member = { id: '', name: 'taro', age: 20 } as const satisfies Member;
console.log(v.is(Member, member));
| Library | バンドルサイズ |
|---|---|
typia | 0.563 kb |
zod | 117 kb |
valibot | 8.50kb |
とても小さいですね!
bundle sizeについて補足
現在の実装でもcreateIsという型をチェックするだけの関数を生成する場合はバンドルサイズがとても小さいです。
しかし、エラーメッセージを生成するための関数を生成しようとすると、なぜかそこにRandom Generatorを含めてしまいバンドルサイズが大きくなってしまうようです。
おそらくzodと同じように Library のかなりの部分を巻き込んでバンドルされているようです。
それでもzodよりは全然小さいですが…
(余談ですが、zodも次のバージョンでバンドルサイズを削減する予定です)
これについては Typia は現在内部のリファクタリングを進めています。
また、先日リリースされたv6.1.0では、本格的に Typia が ESM に対応しました。
これにより、Validationの関数のバンドルサイズは手元だと2/3ほどになりました。
https://github.com/samchon/typia/releases/tag/v6.1.0
開発環境について
ひとりごと
今回の unplugin-typia は npm ではなく JSR で公開されています。 JSR は npm と同じようにパッケージを公開できるサービスですが、npm とは異なり、サーバー上で package.json の生成や TypeScript のコンパイルを開発者の代わりに行ってくれるので、 Libaray の公開がとても簡単です。
欠点としてESMにしか対応していないので、例えば Webpack の設定を書くときは直接 require するとエラーになってしまいますが、jiti などを経由すれば問題ありません。
実際、Webpackの導入方法 では jiti を使った方法を紹介しています。
またローカルでは Bun を使って開発しています。 Bun は TypeScript をそのままimport、実行できるのでとても楽でした。
結果的に bundler 向けの plugin を作っているのに自分は一切 bundler を使わない、という謎な状況になっていますが、開発体験はめちゃくちゃ良かったです。
まあunplugin-typia のコードベースが Bun である必要も特にないので、node_modules を管理しなくて済む Deno に移行するかもしれません。 Deno も直接 TypeScript を実行できますし、何より JSR との相性は抜群ですからね。