samchon

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars0
mentions5
01234Oct 4Oct 18Nov 2
UTC days · repositories samchon owns4 repositories

Package downloads

Cumulative recorded downloads · since 2025-11-02

npm21.7K
05.4K10.9K16.3K21.7KOct 4Oct 18Nov 2
Oct 31npm: 6/6 packages observed

Across 6 known packages in repositories samchon owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

samchon’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside samchon’s account where samchon opened or reviewed pull requests in the past 365 days.

Who noticed samchon

Everyone who starred or forked samchon’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

5posts
Nov 30 posts
  1. Developers using Typia, Anyone using tags.pattern, pattern, MaxLength, or MinLength will see faster validations when they upgrade to Typia 15.2.2 and recompile their project. typia#2504

    3 likes · 516 views
  2. TypeScript の Validation Library 6つを比較してみた!意外だったのは性能じゃなかった話 🤔 みなさん、こんにちは! エビ 🦐 と Type Guard のことばっかり考えているフロントエンドエンジニアの @nyaomaru です!😸 TypeScript で Runtime Validation が必要になると、だいたいこんな話になるよな。 「結局、どの Library を使えばええん?」 Zod? Valibot? ArkType? typia? それとも、自分で Type Guard を書くだけで十分なんちゃう? 僕は is-kit を開発してるので、この境界についてよく考えてる。 そこで最近、こんな疑問が出てきた。 そもそも、同じ問題を解決している Library 同士を比較できてるんやろか? 例えば、欲しいのがこれだけなら、 if (isUser(input)) { input.name; } ほんまに Schema System まで必要なん? でも逆に、 構造化された Validation Error 値の Transformation Parsing Schema Metadata Framework Integration が必要なら、小さな Boolean Guard だけでは足りへんこともある。 そこで今回は「どれが一番ええ Validator なん?」ではなく、ちょっと違う観点で比較してみた。 同じ User の Validation を、以下の7つの方法で実装したで。 手書きの JavaScript Guard + TypeScript の Type Predicate is-kit Runtypes Valibot Zod ArkType typia 6つの OSS と、Library を使わない手書き実装やな。 それぞれを Compile して、同じ Fixture で Test して、Browser Bundle Size を測って、簡単な Steady-state Runtime Benchmark も回した。 で、一番難しかったのは、7つを Compile することじゃなかった。 7つの Validator に、同じ意味の Validation をさせることやった。 例えば「100文字以下の String」という条件ですら、Runtime では違う意味になり得る。 ほんなら、それぞれ何ができて、何を引き換えにしているのか見てこか〜!😺 github.com/nyaomaru 🧪 今回 Validate する User まずは共通の Type から。 type User = { id: string; name: string; age: number; role: "admin" | "user"; tags: string[]; profile: { bio: string; active: boolean; }; }; Runtime で検証する Rule は以下のとおり。 id は String name は UTF-16 Code Unit で1〜100 age は18〜120の整数 role は "admin" または "user" のみ tags は String の Array profile.bio は String profile.active は Boolean Root と profile の余分な Key は拒否 値の Coercion(型変換)はしない 今回の主な Input は、JSON を Decode した普通の Data を想定してる。 ここは重要。 JavaScript には Inherited Property、Sparse Array、Symbol、Getter、Non-enumerable Property みたいな変わった値も作れるからな。 この話は後で戻ってくるで。😹 1. ✍️ 手書き Guard — Guard だけで十分なこともある まずは一番地味な方法。TypeScript の Type Predicate を付けた、手書きの JavaScript Guard や。 Validation Library は使わない。 const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const hasExactKeys = ( value: Record<string, unknown>, keys: readonly string[], ): boolean => Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key)); export const isUser = (value: unknown): value is User => { if ( !isRecord(value) || !hasExactKeys(value, ["id", "name", "age", "role", "tags", "profile"]) ) { return false; } if ( !isRecord(value.profile) || !hasExactKeys(value.profile, ["bio", "active"]) ) { return false; } return ( typeof value.id === "string" && typeof value.name === "string" && value.name.length >= 1 && value.name.length <= 100 && typeof value.age === "number" && Number.isInteger(value.age) && value.age >= 18 && value.age <= 120 && (value.role === "admin" || value.role === "user") && Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string") && typeof value.profile.bio === "string" && typeof value.profile.active === "boolean" ); }; 使う側はこんな感じ。 declare const input: unknown; if (isUser(input)) { input.name.toUpperCase(); } 一箇所だけ Narrowing したいなら、手書き Guard で十分やと思う。 Dependency も新しい抽象化もいらへん。 でも isUser、isProfile、isRole みたいな Guard がどんどん増えてきたら……。 自分だけの小さな Guard Library を保守し始めてる わけやな。😹 そこで次の選択肢が出てくる。👍 2. 🔧 is-kit — 小さな Guard を再利用したくなったら まさにここが、僕が is-kit を作った理由や。 Narrowing のたびに Schema-first の Workflow を使いたかったわけちゃう。 小さな Guard は小さいまま、きれいに Compose できるようにしたかった。 import { and, arrayOf, isBoolean, isNumber, isString, oneOfValues, predicateToRefine, safeParse, struct, } from "is-kit"; export const isName = and( isString, predicateToRefine<string>( (value) => value.length >= 1 && value.length <= 100, ), ); export const isAge = and( isNumber, predicateToRefine<number>( (value) => Number.isInteger(value) && value >= 18 && value <= 120, ), ); export const isProfile = struct( { bio: isString, active: isBoolean, }, { exact: true }, ); export const isUser = struct( { id: isString, name: isName, age: isAge, role: oneOfValues("admin", "user"), tags: arrayOf(isString), profile: isProfile, }, { exact: true }, ); export const parseUser = (input: unknown) => safeParse(isUser, input); 僕にとって大事なのは、struct() で Object を Validate できること自体じゃない。 組み合わせた部品が、普通の Guard としてそのまま使えること や。 const names = values.filter(isName); if (isAge(value)) { // value is number } すでに Predicate を中心に Code を書いているなら、これは便利やと思う。 ただし、is-kit は Full Schema Library を目指しているわけではない。 safeParse() が返すのは小さな Success / Failure の Result。Field ごとの詳細な Issue、Transformation、Schema Metadata を提供するものではない。 そうした機能や Framework Integration が必要なら、僕なら Schema Library を選ぶで。🔧 それと、Type Level で見つかった点も書いておく。 struct() と arrayOf() は Readonly な構造を Inference する。 だから Runtime の Shape は User と一致していても、readonly string[] は Mutable な string[] に Assign できない。 この違いは Cast で隠さず、Type Test でも明示してるで。🔧 3. 🏃 Runtypes — Predicate ではなく Validator Object Runtypes はもう一段違う考え方や。 単に Boolean を返す Predicate ではなく、一つの Runtime Validator Object に複数の使い方が用意されてる。 import { Array as RtArray, Boolean as RtBoolean, Literal, Number as RtNumber, Object as RtObject, String as RtString, Union, } from "runtypes"; const Name = RtString.withConstraint( (value) => (value.length >= 1 && value.length <= 100) || "name length must be 1..100", ); const Age = RtNumber.withConstraint( (value) => (Number.isInteger(value) && value >= 18 && value <= 120) || "age must be an integer in 18..120", ); export const UserSchema = RtObject({ id: RtString, name: Name, age: Age, role: Union(Literal("admin"), Literal("user")), tags: RtArray(RtString), profile: RtObject({ bio: RtString, active: RtBoolean, }).exact(), }).exact(); export const isUser = (input: unknown) => UserSchema.guard(input); export const checkUser = (input: unknown) => UserSchema.check(input); export const parseUser = (input: unknown) => UserSchema.parse(input); export const inspectUser = (input: unknown) => UserSchema.inspect(input); 普通の Predicate と違って、同じ Validator に複数の使い方がある。 guard() で Narrowing check() で失敗時に Throw parse() で Parsing inspect() で構造化された Failure Data を取得 ただし check() と parse() は同じ意味ではない。Parser の挙動によって Object の扱いが変わる場合もある。 単純な Boolean Guard とは違う Runtime の仕組みやな。🏃 4. 🤖 Valibot — Modular な API の Schema Validation Valibot からは、いよいよ Schema Library らしくなってくる。 import * as v from "valibot"; export const UserSchema = v.strictObject({ id: v.string(), name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), age: v.pipe(v.number(), v.integer(), v.minValue(18), v.maxValue(120)), role: v.picklist(["admin", "user"]), tags: v.array(v.string()), profile: v.strictObject({ bio: v.string(), active: v.boolean(), }), }); export const isUser = (input: unknown) => v.is(UserSchema, input); export const parseUser = (input: unknown) => v.safeParse(UserSchema, input); Valibot は Schema Validation、構造化された Issue、Transformation Pipeline を、Modular な Function-based API で組み合わせられる。 この設計は Tree Shaking とも相性がええ。 さらに v.is() は Type Guard として使えるで。🤖 ただし v.is() 自体は詳細な Issue を返さず、Parsing のように Transformation を適用するわけでもない。 5. 💎 Zod — Schema が Ecosystem の一部になるとき 続いて Zod。 import * as z from "zod"; export const UserSchema = z.strictObject({ id: z.string(), name: z .string() .refine( (value) => value.length >= 1 && value.length <= 100, "name must contain 1..100 UTF-16 code units", ), age: z.int().min(18).max(120), role: z.enum(["admin", "user"]), tags: z.array(z.string()), profile: z.strictObject({ bio: z.string(), active: z.boolean(), }), }); export type User = z.output<typeof UserSchema>; export const parseUser = (input: unknown) => UserSchema.safeParse(input); // This schema has no transforms, so its output type is safe for the input too. export const isUser = (input: unknown): input is User => parseUser(input).success; Zod に組み込みの is() Method はないけど、この Adapter は元の input を Narrowing できる。 今回の Schema には Transform がないからやな。 Transform を含む Schema なら、元の Input と Output の Type が違う可能性があるので、safeParse().data を Typed Value として使うほうが安全。 ここは混同したらアカンところ。 さて、Zod の魅力は User を Validate できること自体ではない。 それなら今回のどの実装でもできる。 大きいのは Schema を取り巻く Ecosystem や。 Form Integration API Tooling Schema Conversion Metadata Codec OpenAPI 関連 Tooling Framework Integration Schema が Application Infrastructure の一部になっているなら、こうした Ecosystem は強い選択理由になる。 String Length で気になったこと 今回、これは使わなかった。 z.string().min(1).max(100); なんでかって? 今回の Contract は、 1〜100 UTF-16 Code Unit やから。 Zod の組み込み String Length Check は Unicode Code Point を数えるので、Contract に合わせるため .refine() と JavaScript の string.length を使った。 後で出てくる Emoji の Fixture で、違いがはっきりするで。😹 Bundle Size はどうなん? 「Valibot は小さいけど、Zod は大きい」 そう単純にも言えへん。Zod には Functional で Tree-shakable な Zod Mini もある。 後半の Bundle Size は 通常版 Zod の結果で、Zod Mini の測定ではないで。 また Zod は z.compile() で Runtime の Validation Logic を最適化できるけど、今回は有効にしていない。 これは typia の Build 時の TypeScript Transform とは別の仕組みや。💎 6. 🚢 ArkType — Runtime Schema が TypeScript っぽかったら? ArkType は Schema の書き方がかなり違う。 import { type } from "arktype"; export const UserSchema = type({ id: "string", name: "0 < string <= 100", age: "18 <= number.integer <= 120", role: "'admin' | 'user'", tags: "string[]", profile: { bio: "string", active: "boolean", }, }).onDeepUndeclaredKey("reject"); export const isUser = (input: unknown) => UserSchema.allows(input); export const parseUser = (input: unknown) => UserSchema(input); export const ArkErrors = type.errors; この比較、結構好きやねんな。 z.int().min(18).max(120); と、 "18 <= number.integer <= 120"; どっちが絶対に優れている、という話ではない。 同じ Runtime Contract を表現するための、違う書き方や。 TypeScript っぽい式が自然に読める Team なら、ArkType はかなり読みやすいと思う。 allows() は Type Guard として使えるし、Schema を直接呼ぶと Validated Output か Error Collection を取得できる。 もう一つ面白いのは、new Function が使える Environment なら Runtime で Traversal Logic を Precompile できること。 つまり ArkType も、すでに最適化された Validation Logic を作っている。 でも typia と同じ Architecture ではない。 ArkType は Runtime の Type Representation から始まる。 typia は Build 時の TypeScript Type から始まるんや。🚢 7. 🚀 typia — Build 時に Validator を生成する 今回の中で、typia は特に毛色が違う。 import typia, { type tags } from "typia"; type Utf16Name = tags.TagBase<{ kind: "utf16Name"; target: "string"; value: undefined; validate: "$input.length >= 1 && $input.length <= 100"; }>; export interface User { id: string; name: string & Utf16Name; age: number & tags.Type<"uint32"> & tags.Minimum<18> & tags.Maximum<120>; role: "admin" | "user"; tags: string[]; profile: { bio: string; active: boolean; }; } export const isUser = typia.createEquals<User>(); export const validateUser = typia.createValidateEquals<User>(); typia は TypeScript の Type と Tag を、Build 時に専用の Validation Code に変換する。Runtime Schema Object をその場で解釈する方式とは違う。🚀 ただし、Trade-off もある。 Build Integration が必要。 今回の公式 Setup は TypeScript 7 と ttsc / typia の Transform Integration を使う。 普通の tsc だけでは Transform は走らない。 僕の検証環境でも、最初の typia Build では Go の Source Plugin を Compile する必要があり、数分かかった。 この Setup Cost は Runtime Benchmark には入っていないけど、採用を決めるときには無視できへんよな。 それから、組み込みの MinLength / MaxLength は今回の UTF-16 Contract と一致しなかった。 だから name には Custom Tag を使ってる。 またしても「同じような Rule に見えるのに、実際の意味が違う」という話や。🚀 😹 えっ、String Length って同じじゃないん? 最初に「えっ?」となったのがこの Test。 "😀".repeat(51).length; // 102 51 Unicode Code Point なのに、102 UTF-16 Code Unit になる。 今回の Contract は最大100 UTF-16 Code Unit。だから Reject されるべきや。 実際、最初から Reject したのは手書き Guard、is-kit、Runtypes、Valibot、ArkType。 一方、最初の Zod と typia の Sample は Accept した。なんでやねん!😹 組み込みの Length Constraint が Unicode Code Point を数えていたからや。 別にどっちが間違いというわけではない。 違う Runtime Contract を表している というだけ。 さらに、人間から見た「1文字」とも必ずしも一致しない。Grapheme Cluster まで考えたら Unicode はもっと面白いことになる。😹 だから、最終的には Zod を .refine() に、typia を Custom Tag に変更した。 今回の比較で、個人的に一番大きかった発見がこれ。 同じように見える Schema が、同じ Runtime Contract を表しているとは限らない。 で、結局何を比較してるん? ここまで見ると、この7つは単なる交換可能な Library には見えへんよな。 そもそも Runtime Model が違う。 まず表で整理してみよう。 Approach Source of truth Natural success API Structured failures Transformation model Compilation / build requirement Handwritten guard Type + handwritten check Predicate Manual Manual None is-kit Composed guards Predicate / safeParse Basic valid/invalid result Guard-oriented None Runtypes Runtype object guard / check / parse Yes Parsers None Valibot Schema functions is / safeParse Yes Schema pipelines None Zod Schema objects parse / safeParse Yes Transforms / codecs No build transform; optional runtime z.compile() ArkType Runtime type expressions allows / invocation Yes Morphs No build transform; runtime precompilation when available typia TypeScript types and tags Generated predicates / validation results With validation APIs Build-generated functions Build transform required だから、これらを「上位互換への進化段階」みたいに考えるのは違うと思うようになった。 Schema Library は「より優れた Type Guard」とは限らない。 Generated Validator も「より優れた Schema」とは限らない。 問題が重なる部分はあるけど、解決している Layer が違うんやな。 ✅ 実際に何が Pass したん? 同じ35個の JSON Fixture で7つの実装を検証した。 35 fixtures × 7 validators = 245 checks 245個、すべて Pass! ✅ 境界値、Emoji の Length、間違った型、Missing Field、余分な Key などを確認してる。Harness では Type Narrowing、Input Mutation、Build、Bundle 後の動作も検証した。 再現してみたい人はこちら。😺 Harness、Lockfile、Fixture、Probe、Benchmark の結果は、固定 Commit の typescript-validator-comparison に公開してるで。 git clone nyaomaru/typescript-validator-comparison cd typescript-validator-comparison git checkout --detach b26350d01fd18cb77d55a95e2f3b112657f0d305 corepack pnpm install --frozen-lockfile corepack pnpm verify 今回の JSON に相当する Contract では7つの実装が一致した、という結果や。あらゆる JavaScript Value に対する完全な一致を意味するわけではないで。 🧩 もう一つの驚き。「Exact」って本当に Exact なん? 普通の JSON に収まらない Value も Test してみた。 表に出す4つの Probe は、この Code に対応してる。 const sparseTags = { ...valid, tags: new Array(2) }; const inheritedFields = Object.create(valid); const extraSymbol = { ...valid, [Symbol("extra")]: true }; const extraUndefined = { ...valid, extra: undefined }; Repository では、このほかに Non-enumerable Property や Throw する Getter を含む、さらに9つの Probe を実行している。 この4つの結果はこんな感じ。 Probe Native is-kit Runtypes Valibot Zod ArkType typia Sparse tags array Accept Reject Accept Reject Reject Reject Accept All fields inherited Reject Reject Reject Accept Accept Accept Accept Extra symbol property Accept Accept Reject Accept Accept Reject Accept Extra key with value undefined Reject Reject Reject Reject Reject Reject Accept さらに、Test した余分な Non-enumerable Property は7つすべてで Accept された。 そして Throw する Getter を読み取るケースでは、7つとも Throw した。 どれかの Library が壊れている、という話ではない。 「Exact Object Validation」には、対象範囲の定義が必要 ということや。 手書き Guard の Object.keys() は Symbol や Non-enumerable Key を見ないし、Array.every() は Sparse Array の Hole を Skip する。 JSON Data なら合理的でも、あらゆる JavaScript Object に共通する Rule ではないんやな。😺 📦 Bundle Size さて、みんな気になる Bundle Size や!😺 Browser 向けの各 Entry は validate だけを Export している。 設定はすべて同じ esbuild の条件に揃えた。 Browser Platform ESM ES2022 Minify Tree Shaking Legal Comment を削除 Raw、gzip、Brotli の Size を測った結果がこれ。 Validator Raw bytes gzip bytes Brotli bytes handwritten guard 588 337 295 is-kit 3,421 1,530 1,423 runtypes 16,667 5,188 4,657 valibot 5,722 1,853 1,675 zod 89,888 26,212 23,331 arktype 155,549 47,603 41,956 typia 772 403 339 結果を見る前に、3つだけ注意してな。👇 Standalone Cost: 独立した Bundle の Size。既存 Application に Schema を1つ追加する Cost とは違う。 通常版 Zod: Zod Mini は測っていない。 Generated Code: typia の小さな Bundle にも生成 Code と Helper は含まれる。 それでも、Architecture の違いが表れているのは面白いな。😺 ⚡ Runtime Performance 同じ Contract で、簡単な Steady-state Benchmark も回してみた。 Case は正常な User、間違った id、間違った profile.active の3つ。 各 Library を別々の Node Process で実行し、20,000 Call の Warmup 後に、50,000 Call × 9 Sample を測定した。Schema の生成は測定 Loop の外や。 結果は 1 Call あたりの Median(ns)。括弧内は Min〜Max。 これは Microbenchmark なので、V8 の最適化や API の処理内容の違いに注意してな。 Validator Valid (ns/call) Wrong id (ns/call) Wrong profile.active (ns/call) handwritten guard 79.9 (77.3–114.1) 73.6 (72.1–84.9) 80.8 (79.0–96.3) is-kit 232.0 (225.7–257.5) 34.5 (33.1–38.9) 210.3 (203.6–218.1) runtypes 5804.0 (5425.5–6298.0) 9221.8 (8443.2–11063.2) 10101.2 (9287.6–10503.5) valibot 951.2 (425.2–1782.7) 200.2 (96.1–422.3) 1112.1 (648.4–1538.1) zod 612.6 (351.3–1203.5) 1491.6 (862.9–2824.6) 2208.5 (1592.4–4757.4) arktype 291.5 (278.1–614.4) 9.6 (6.6–17.7) 12.0 (11.3–37.6) typia 21.9 (20.5–46.1) 2.0 (1.9–6.4) 11.5 (10.8–17.2) ただし、これを 「typia がどんな状況でも最速!」 と読むのは違うで。😹 同じ小さな Object を繰り返しているので、2.0 ns のような値を実際の Application にそのまま一般化はできへん。 それでも、面白い傾向はあった。👇 🚀 typia はこの Steady-state Test で Overhead が小さかった。 🚢 ArkType は Invalid Input の処理が特に速かった。 🔧 is-kit は間違った id を早い段階で Reject した。 🏃 Runtypes は今回の Loop では比較的 Cost が高かった。 Architecture と Failure Path の両方が大事、ということやな。 🤔 ほな、結局どれを使う? ここまで検証した結果、僕の選び方は結構シンプルになった。 やりたいこと 候補 一箇所だけ Type Check ✍️ 手書き Guard 再利用できる Guard を Compose 🔧 is-kit Rich な Validator API 🏃 Runtypes Modular な Schema Validation 🤖 Valibot Schema の Ecosystem や Integration 💎 Zod TypeScript っぽい Schema Syntax 🚢 ArkType Build 時に Type から Validator を生成 🚀 typia 万人向けの勝者はおらへん。 😺 今回一番驚いたのは、Bundle Size や Speed ではなく、7つの Validator が、ちょっとずつ違う Runtime Contract を表現し得る ということ。 だから Library を選ぶ前に考えたい。 何を Validate して、結果として何が必要なん? 一番小さくて速いものが、その Boundary にとって一番合うとは限らへん。 みんななら、どれを使う?🤔 最後まで読んでくれてありがとう〜!🤲🦐

  3. I Compared 6 TypeScript Validation Libraries. Here's What Surprised Me 🤔 Hoi hoi! 👋   I'm nyaomaru, a frontend engineer and OSS developer working mostly with TypeScript. When we need runtime validation in TypeScript, the conversation often turns into this 👇 > "Which library should I use?" - Zod? - Valibot? - ArkType? - typia? Or maybe, just write a type guard? I maintain is-kit, so I think about this boundary quite a lot. And recently I started wondering > Are we comparing libraries that are actually solving the same problem? For example, if all I want is this if (isUser(input)) { input.name; } do I really need a schema system? But if I want: - structured validation errors - transformations - parsing - schema metadata - framework integrations then a small boolean guard is probably not enough either. So instead of asking "Which validator is best?", I tried a different comparison. I implemented the same User validation with: - a handwritten guard with a TypeScript predicate - is-kit - Runtypes - Valibot - Zod - ArkType - typia Then I compiled, tested them against the same fixtures, measured standalone browser bundle size, and ran a small steady-state runtime benchmark. The hardest part was not making seven validators compile. It was making seven validators mean the same thing. Even “a string with at most 100 characters” can describe different runtime contracts. So, let's look at what each tool is actually buying us. 😺 !TypeScript Validation Battle --- 🧪 The User we will validate Here is the shared type. type User = { id: string; name: string; age: number; role: "admin" | "user"; tags: string[]; profile: { bio: string; active: boolean; }; }; And the runtime rules are: - id must be a string - name must contain 1–100 UTF-16 code units - age must be an integer from 18 to 120 - role must be exactly "admin" or "user" - tags must be an array of strings - profile.bio must be a string - profile.active must be a boolean - extra keys are rejected at the root and inside profile - values are not coerced For this comparison, the main input domain is ordinary JSON-decoded data. That matters because JavaScript can also give us strange things like inherited properties, sparse arrays, symbols, getters, and non-enumerable keys. We'll come back to those later. --- 1. ✍️ Handwritten Guard — Sometimes a Guard Is Enough Let's start with the boring option, a handwritten JavaScript guard with a TypeScript predicate. No validation library. const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const hasExactKeys = ( value: Record<string, unknown>, keys: readonly string[], ): boolean => Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key)); export const isUser = (value: unknown): value is User => { if ( !isRecord(value) || !hasExactKeys(value, ["id", "name", "age", "role", "tags", "profile"]) ) { return false; } if ( !isRecord(value.profile) || !hasExactKeys(value.profile, ["bio", "active"]) ) { return false; } return ( typeof value.id === "string" && typeof value.name === "string" && value.name.length >= 1 && value.name.length <= 100 && typeof value.age === "number" && Number.isInteger(value.age) && value.age >= 18 && value.age <= 120 && (value.role === "admin" || value.role === "user") && Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string") && typeof value.profile.bio === "string" && typeof value.profile.active === "boolean" ); }; And then declare const input: unknown; if (isUser(input)) { input.name.toUpperCase(); } This is still very good. For one local boundary, a handwritten type guard has a huge advantage > There is no extra model to learn. It is just JavaScript runtime checks plus a TypeScript predicate. But the cost starts to appear when your codebase grows this isUser; isProfile; isRole; isNonEmptyString; isAdultAge; isStringArray; isApiResponse; isConfig; isFeatureFlag; Now you are not writing "one guard" anymore. You are maintaining your own tiny guard library. And this is where the next option starts to make sense. 👍 --- 2. 🔧 is-kit — When Small Guards Become Reusable Building Blocks This is the problem I built is-kit around. I didn't want a schema-first workflow for every narrowing problem. I wanted small guards that stay small, but compose cleanly. import { and, arrayOf, isBoolean, isNumber, isString, oneOfValues, predicateToRefine, safeParse, struct, } from "is-kit"; export const isName = and( isString, predicateToRefine<string>( (value) => value.length >= 1 && value.length <= 100, ), ); export const isAge = and( isNumber, predicateToRefine<number>( (value) => Number.isInteger(value) && value >= 18 && value <= 120, ), ); export const isProfile = struct( { bio: isString, active: isBoolean, }, { exact: true }, ); export const isUser = struct( { id: isString, name: isName, age: isAge, role: oneOfValues("admin", "user"), tags: arrayOf(isString), profile: isProfile, }, { exact: true }, ); export const parseUser = (input: unknown) => safeParse(isUser, input); The important part is that the pieces remain normal guards const names = values.filter(isName); if (isAge(value)) { // value is number } That is useful when your codebase already thinks in terms of predicates. And this is also where I would draw the line. safeParse() gives a small success/failure result around a guard. It does not turn is-kit into: - a field-level issue system - a transformation pipeline - a schema metadata system - an OpenAPI ecosystem - a form integration layer If those are the actual requirements, I would rather use a schema library than try to make a guard toolkit become one. One type-level detail also showed up in the verification, struct() and arrayOf() infer readonly structures. So the runtime shape matches our User, but readonly string[] is not assignable to mutable string[]. That difference is real, and the type tests make it explicit instead of hiding it with a cast. 🔧 --- 3. 🏃 Runtypes — A Validator Object, Not Just a Predicate Runtypes moves one level further. Instead of just a boolean predicate, you get a runtime validator object with several ways to consume it. import { Array as RtArray, Boolean as RtBoolean, Literal, Number as RtNumber, Object as RtObject, String as RtString, Union, } from "runtypes"; const Name = RtString.withConstraint( (value) => (value.length >= 1 && value.length <= 100) || "name length must be 1..100", ); const Age = RtNumber.withConstraint( (value) => (Number.isInteger(value) && value >= 18 && value <= 120) || "age must be an integer in 18..120", ); export const UserSchema = RtObject({ id: RtString, name: Name, age: Age, role: Union(Literal("admin"), Literal("user")), tags: RtArray(RtString), profile: RtObject({ bio: RtString, active: RtBoolean, }).exact(), }).exact(); export const isUser = (input: unknown) => UserSchema.guard(input); export const checkUser = (input: unknown) => UserSchema.check(input); export const parseUser = (input: unknown) => UserSchema.parse(input); export const inspectUser = (input: unknown) => UserSchema.inspect(input); This feels different from a guard toolkit. A predicate mainly answers > Is this value a User? A Runtype can also answer that question, but the same object can: - narrow with guard - throw with check - parse with parse - expose structured failure data Also, check() and parse() should not be treated as synonyms. Runtypes supports parser behavior, and object handling can differ between validation and parsing contexts. That is exactly the kind of runtime machinery you do not have in a plain predicate. 🏃 --- 4. 🤖 Valibot — Schema Validation With a Modular API Valibot is where the comparison starts looking more "schema-like". import * as v from "valibot"; export const UserSchema = v.strictObject({ id: v.string(), name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), age: v.pipe(v.number(), v.integer(), v.minValue(18), v.maxValue(120)), role: v.picklist(["admin", "user"]), tags: v.array(v.string()), profile: v.strictObject({ bio: v.string(), active: v.boolean(), }), }); export const isUser = (input: unknown) => v.is(UserSchema, input); export const parseUser = (input: unknown) => v.safeParse(UserSchema, input); This gives us: - schema validation - type inference - structured issues - parsing APIs - transformation pipelines But the API is still function-based and modular. That is a big part of Valibot's design. Instead of every capability living as methods on large schema objects, you compose independent functions. That makes tree shaking a first-class part of the normal API. And unlike a pure schema-only mental model, Valibot also exposes v.is() as a type guard. So schema validation and ordinary TypeScript narrowing are not mutually exclusive. One caveat: v.is() is the guard path. It does not expose validation issues or apply transformations like parsing does. 🤖 --- 5. 💎 Zod — When the Schema Is Part of a Bigger Ecosystem Now Zod. import * as z from "zod"; export const UserSchema = z.strictObject({ id: z.string(), name: z .string() .refine( (value) => value.length >= 1 && value.length <= 100, "name must contain 1..100 UTF-16 code units", ), age: z.int().min(18).max(120), role: z.enum(["admin", "user"]), tags: z.array(z.string()), profile: z.strictObject({ bio: z.string(), active: z.boolean(), }), }); export type User = z.output<typeof UserSchema>; export const parseUser = (input: unknown) => UserSchema.safeParse(input); // This schema has no transforms, so its output type is safe for the input too. export const isUser = (input: unknown): input is User => parseUser(input).success; Zod does not expose a built-in is() method, but this adapter can narrow input because this schema has no transforms. With a transforming schema, safeParse().data is the safe typed value: the original input and output types may differ. Zod is not interesting because it can validate a User. All of these libraries can do that. The real value is everything around the schema: - form integrations - API tooling - schema conversion - metadata - codecs - OpenAPI-related tooling - framework integrations If your schema is already becoming part of application infrastructure, that ecosystem matters. 💎 There is one important detail in this sample though. I did not use this z.string().min(1).max(100); Why? Because our contract says > 1–100 UTF-16 code units Zod's built-in string length checks use Unicode code points, so this sample uses .refine() with JavaScript's string.length to preserve the stated UTF-16 contract. The shared emoji fixture demonstrates why below. What about bundle size? It is too easy to write > Valibot is small, Zod is large. That is not a good 2026 comparison anymore. Zod also has Zod Mini, which uses a more functional, tree-shakable API. The bundle table later in this article measures regular Zod, not Zod Mini. So it should not be read as Valibot versus the smallest possible Zod setup. Zod can also compile optimized validation logic at runtime with z.compile() and new Function. I did not enable it here. That makes this distinction more useful than the broad label “generated validation” runtime schema compilation vs build-time generation from TypeScript types That distinction becomes important when we get to typia. --- 6. 🚢 ArkType — What If the Runtime Schema Looked Like TypeScript? ArkType has a very different authoring model. import { type } from "arktype"; export const UserSchema = type({ id: "string", name: "0 < string <= 100", age: "18 <= number.integer <= 120", role: "'admin' | 'user'", tags: "string[]", profile: { bio: "string", active: "boolean", }, }).onDeepUndeclaredKey("reject"); export const isUser = (input: unknown) => UserSchema.allows(input); export const parseUser = (input: unknown) => UserSchema(input); export const ArkErrors = type.errors; I like this comparison z.int().min(18).max(120); vs "18 <= number.integer <= 120"; Neither is "better". They are different ways to express the runtime contract. If your team already thinks naturally in TypeScript-like expressions, ArkType is very readable. allows() also works as a type guard. And calling the schema directly gives you validated output or an error collection. Another interesting part: ArkType can precompile optimized traversal logic at runtime when the environment allows new Function. So ArkType is already doing specialization. 🚢 But it is still not the same architecture as typia. ArkType starts from a runtime type representation. typia starts from TypeScript types during the build. --- 7. 🚀 typia — Generate the Validator During the Build typia is the outlier in this comparison. import typia, { type tags } from "typia"; type Utf16Name = tags.TagBase<{ kind: "utf16Name"; target: "string"; value: undefined; validate: "$input.length >= 1 && $input.length <= 100"; }>; export interface User { id: string; name: string & Utf16Name; age: number & tags.Type<"uint32"> & tags.Minimum<18> & tags.Maximum<120>; role: "admin" | "user"; tags: string[]; profile: { bio: string; active: boolean; }; } export const isUser = typia.createEquals<User>(); export const validateUser = typia.createValidateEquals<User>(); This is not a runtime schema object. The source of truth is the TypeScript type plus typia's tags. Then typia's build transform turns typia.createEquals<User>(); into specialized validation code. That means there is no general runtime schema interpreter walking a User schema object. But there is a trade-off > You need build integration. The current official setup uses TypeScript 7 and ttsc / typia's transform integration. Stock tsc alone does not run the transform. In my verification workspace, the first typia build also had to compile a Go source plugin and took several minutes. That setup cost is not part of the runtime benchmark, but it is absolutely part of the adoption decision. Also, typia's built-in MinLength / MaxLength did not match our UTF-16 contract. So I used a custom tag for name. Again, same-looking validation rule, different semantics. 🚀 --- 😹 Wait, Even "String Length" Was Different? This was the first test that made me stop. "😀".repeat(51).length; // 102 JavaScript's string.length counts UTF-16 code units. So with our contract maximum = 100 UTF-16 code units this should fail. And it did fail in: - handwritten guard - is-kit - Runtypes - Valibot - ArkType But the original Zod and typia samples accepted it. Why? Because their built-in length constraints count Unicode code points instead. For this string 51 emoji = 51 code points = 102 UTF-16 code units Neither behavior is wrong. They are just different contracts. And neither one necessarily matches "what a human sees" either, because grapheme clusters make Unicode even more fun. 😹 So I changed the Zod sample to .refine() and typia to a custom tag. That was probably the most useful lesson in the whole comparison 👇 > Equivalent-looking schemas are not automatically equivalent runtime contracts. --- So What Are We Actually Comparing? At this point, these no longer look like seven interchangeable libraries. They are different runtime models. | Approach | Source of truth           | Natural success API                       | Structured failures       | Transformation model       | Compilation / build requirement                           | | --------- | -------------------------- | ------------------------------------------ | --------------------------- | -------------------------- | ---------------------------------------------------------- | | Handwritten guard | Type + handwritten check   | Predicate                                 | Manual                     | Manual                     | None                                                       | | is-kit   | Composed guards           | Predicate / safeParse                   | Basic valid/invalid result | Guard-oriented             | None                                                       | | Runtypes | Runtype object             | guard / check / parse               | Yes                         | Parsers                   | None                                                       | | Valibot   | Schema functions           | is / safeParse                         | Yes                         | Schema pipelines           | None                                                       | | Zod       | Schema objects             | parse / safeParse                     | Yes                         | Transforms / codecs       | No build transform; optional runtime z.compile()         | | ArkType   | Runtime type expressions   | allows / invocation                     | Yes                         | Morphs                     | No build transform; runtime precompilation when available | | typia     | TypeScript types and tags | Generated predicates / validation results | With validation APIs       | Build-generated functions | Build transform required                               | This is why I no longer like thinking about them as a simple upgrade ladder. A schema library is not "a better type guard". A generated validator is not "a better schema". They solve overlapping problems at different layers. --- ✅ What Actually Passed? The complete harness, lockfile, fixtures, probes, and generated result files live in typescript-validator-comparison. The article links to that immutable commit, rather than to a moving branch. To reproduce the recorded snapshot: git clone https://github.com/nyaomaru/typescript-validator-comparison.git cd typescript-validator-comparison git checkout --detach b26350d01fd18cb77d55a95e2f3b112657f0d305 corepack pnpm install --frozen-lockfile corepack pnpm verify pnpm verify runs lint, strict type checking, fixtures, bundle measurement, benchmarks, JavaScript-only probes, and report generation. The result tables below are that run's saved snapshot, reruns on another machine will naturally produce different timing numbers. I pinned the versions and ran the same fixture matrix against all seven implementations. Environment Date:       2026-10-23 Node:       24.22.1 TypeScript: 7.0.2 pnpm:       11.22.0 ttsc:       0.31.3 tsbuild:    0.28.2 Packages | Package   | Version | | --------- | -------- | | is-kit   | 1.15.3   | | runtypes | 7.0.5   | | valibot   | 1.5.0   | | zod       | 4.6.5   | | arktype   | 2.2.8   | | typia     | 15.1.0   | The shared matrix contains 35 JSON fixtures. That includes: - valid users - age boundaries - name boundaries - emoji length cases - wrong primitive types - missing fields - invalid roles - invalid array elements - extra root keys - extra nested keys - non-object roots All seven matched the expected result. 35 fixtures × 7 validators = 245 boolean checks All 245 passed. The harness also checks input mutation, richer APIs, strict TypeScript compilation, lint, builds, narrowing, and bundled output behavior. So the implementations agree for the JSON-shaped contract in this article—not necessarily for every JavaScript value. --- 🧩 One More Surprise: "Exact" Is Not Always Exact I also tested values outside the normal JSON-shaped input domain. The four probes in the table mean exactly this: const sparseTags = { ...valid, tags: new Array(2) }; const inheritedFields = Object.create(valid); const extraSymbol = { ...valid, [Symbol("extra")]: true }; const extraUndefined = { ...valid, extra: undefined }; The repository runs nine more probes, including non-enumerable properties and a throwing getter. Here is what happened for these four | Probe                             | Native | is-kit | Runtypes | Valibot | Zod     | ArkType | typia   | | --------------------------------- | ------ | ------ | -------- | -------- | ------- | -------- | ------- | | Sparse tags array               | Accept | Reject | Accept   | Reject   | Reject | Reject   | Accept | | All fields inherited             | Reject | Reject | Reject   | Accept   | Accept | Accept   | Accept | | Extra symbol property             | Accept | Accept | Reject   | Accept   | Accept | Reject   | Accept | | Extra key with value undefined | Reject | Reject | Reject   | Reject   | Reject | Reject   | Accept | All seven accepted the extra non-enumerable property I tested. All seven threw when reading a getter that throws. This does not mean one library is "correct" and the others are broken. It means > "Exact object validation" needs a scope. For example, my native guard uses Object.keys(value); That only sees enumerable string keys. And this value.tags.every(...) skips sparse array holes. That is perfectly reasonable for JSON data. It is not a universal policy for every weird object JavaScript can create. This is another reason I think validator comparisons should state the expected input domain. --- 📦 Bundle Size Now the fun table. 😺 Each browser-targeted entry exports only validate. All of them were bundled with the same esbuild settings: - browser platform - ESM - ES2022 - minification - tree shaking - legal comments removed Then I measured raw, gzip, and Brotli output. | Validator | Raw bytes | gzip bytes | Brotli bytes | | ---------- | --------: | ---------: | -----------: | | handwritten guard |       588 |        337 |          295 | | is-kit     |     3,421 |      1,530 |        1,423 | | runtypes   |    16,667 |      5,188 |        4,657 | | valibot   |     5,722 |      1,853 |        1,675 | | zod       |    89,888 |     26,212 |       23,331 | | arktype   |   155,549 |     47,603 |       41,956 | | typia     |       772 |        403 |          339 | A few important caveats. This is standalone cost If your app already ships Zod, the marginal cost of one more schema is not 26 KB gzip. This table measures isolated entry bundles. This is regular Zod Not Zod Mini. So this table does not answer > Valibot vs Zod Mini? Generated does not mean zero bytes typia's final guard bundle was only 772 bytes raw in this test. But it still shipped generated code and a uint32 helper. "Generated validator" does not mean "free validator". Still, the architectural difference is very visible here. --- ⚡ Runtime I almost did not include runtime numbers because validator benchmarks get misleading very quickly. But since the workspace already had the same contract and fixtures, I ran a small steady-state benchmark. Three cases: - valid user - wrong id - wrong nested profile.active Each library ran in a separate Node process. Each case used: - 20,000 warmup calls - 9 measured samples - 50,000 calls per sample Schemas and validators were created outside the measured loop. Results are median nanoseconds per call, with min–max in parentheses. | Validator |                  Valid |              Wrong id |   Wrong profile.active | | ---------- | ---------------------: | ----------------------: | -----------------------: | | handwritten guard |      79.9 (77.3–114.1) |       73.6 (72.1–84.9) |        80.8 (79.0–96.3) | | is-kit     | 232.0 (225.7–257.5) | 34.5 (33.1–38.9) | 210.3 (203.6–218.1) | | runtypes   | 5804.0 (5425.5–6298.0) | 9221.8 (8443.2–11063.2) | 10101.2 (9287.6–10503.5) | | valibot   |    951.2 (425.2–1782.7) |     200.2 (96.1–422.3) |   1112.1 (648.4–1538.1) | | zod       |   612.6 (351.3–1203.5) | 1491.6 (862.9–2824.6) | 2208.5 (1592.4–4757.4) | | arktype   |    291.5 (278.1–614.4) |           9.6 (6.6–17.7) |         12.0 (11.3–37.6) | | typia     |      21.9 (20.5–46.1) |           2.0 (1.9–6.4) |         11.5 (10.8–17.2) | Please do not read this as: > typia is the fastest validator. This is a microbenchmark. The same small object shapes are validated repeatedly, so V8 can optimize very aggressively. I would not interpret a 2.0 ns result as a portable "cost of validation" in a real application. The success APIs are also different: - handwritten guard uses a predicate - is-kit uses a predicate - Runtypes uses .guard() - Valibot uses is() - Zod uses safeParse(...).success - ArkType uses allows() - typia uses a generated equals guard Those operations do not all perform exactly the same work. What the benchmark _does_ show is that architecture and failure path matter. For example: - the handwritten guard pays for exact-key checks before rejecting the wrong id - ArkType's invalid paths were much cheaper than its valid path - typia's generated guard had very low steady-state overhead here - Runtypes' richer validation machinery costs more in this particular hot loop That is useful. A universal speed ranking is not. --- So... Which One Would I Use? 🤔 After doing all of this, my decision tree is actually pretty simple. Just one local narrowing check? Use a handwritten guard. if (isUser(input)) { // input is User } No dependency is a feature when the problem is small. Lots of reusable guards? This is where I use is-kit. 🔧 values.filter(isName); if (isAge(value)) { // ... } The problem is not "I need a schema". The problem is "I keep rebuilding the same predicates and composition helpers". Want a validator object? Runtypes is interesting when you like having one runtime object that can: 🏃 guard check parse inspect assert Want schema validation with a modular API? Valibot. 🤖 Especially if client bundle size and tree shaking are important parts of the design. Want schemas tied into a bigger ecosystem? Zod. 💎 That is probably its strongest reason to exist in many applications. And if bundle size is a concern, evaluate Zod Mini separately. Want the schema syntax to feel like TypeScript? ArkType. 🚢 "18 <= number.integer <= 120"; is a very different authoring experience from chained schema methods. Want TypeScript types transformed into validators during the build? typia. But I would treat the build integration as part of the decision, not a footnote. --- Final Thought These tools overlap, but they live at different layers. The hard part is not making seven validators compile. It is making seven validators mean the same thing. So before comparing package size or benchmark screenshots, ask what the boundary needs to return and what runtime machinery is worth carrying. > Use the smallest model that still represents your boundary well. Then, which one will you use? Thank you for reading! 🤲

    64 reactions · 26 comments
  4. api-contract-check — библиотека для генерации валидаторов из TS-кода и проверки API-запросов Продолжаю делиться выложенными в OpenSource библиотеками, которые использую в работе. В этот раз речь пойдет о наиболее универсальном, на мой взгляд, подходе для валидации приходящих с бэкенда данных: в клиентской части пишутся обычные DTO-модели на TypeScript, а валидаторы создаются автоматически. Вот ссылки на документацию на русском и английском, а в статье я сравню различные подходы к валидации. Зачем нужна валидация API В большинстве проектов, которые видел, валидация не производилась вообще — вызовы XHR / fetch выполнялись в componentDidMount / useEffect или аналогах для других фреймворков. Это становилось крупнейшей точкой отказа из-за ручных ошибок, внезапного изменения контрактов со стороны бэкенда или просто несогласованности, когда бэкенд обещает string, но присылает null или поле и вовсе отсутствует. В обратную сторону это тоже работает — фронтенд может отправлять некорректные или неиспользуемые бэкендом поля. Интеграция со внешними системами в целом требует валидации для стабильности, иначе поиск рассогласований будет долгим и дорогим, так как ошибки могут всплыть в совершенно неожиданных местах, а не в точке запроса за данными. Поэтому было создано несколько подходов, которые решали вышеперечисленные проблемы. Подход 1: генерация из OpenAPI схем В крупных энтерпрайз-проектах мелочиться не стали. Бэкенд генерировал OpenAPI схемы (например, для отображения в Swagger) и из них либо создавались только TS-типы (минимальный подход), либо создавались готовые эндпоинты для вызова запросов за данными (условно, api.getUser уже типизирован и вызывает fetch на нужную точку) с опциональным встраиванием runtime-валидаторов. Этот подход у многих применяется до сих пор, но количество недостатков слишком велико: сгенерированная схема может по факту расходиться с тем, что присылает бэкенд. На практике это происходит часто и зависит от используемых фреймворков на бэкенде. Например, при param?: string может прийти null вместо отсутствующего параметра, либо из-за сложности устройства типов на бэкенде схема получается неполной версионирование становится очень сложным. Например, для задачи в новой ветке бэкендер опубликовал npm-пакет "@api-spec": "123.3.1877-feature-123", однако в dev-ветку уже вмерджены изменения от других разработчиков, что может вызывать конфликты. Нужны постоянные rebase, перегенерация, переустановка зависимостей, иногда — отдельные инстансы Swagger для тестировщиков. При большом потоке изменений и при использовании нескольких бэкендов, которые генерируют разные npm-пакеты вроде "@api-spec-payments"/"@api-spec-geo", эту рутину приходится выполнять несколько раз в день, а затраты на инфраструктуру растут фронтенд и бэкенд не могут работать над задачей параллельно — обязательно должен сначала быть реализован (хотя бы на моках) бэкенд, чтобы сгенерировать новый пакет. Либо все скатывается в ручной обмен спецификациями в чате, со всей сопутствующей непрозрачностью и отставанием от мастер-веток фронтенд может использовать только 1 свойство, а бэкенд присылает 100 (возможно, это легаси, техдолг или ориентация на разных фронтенд-потребителей). Сгенерированный по OpenAPI клиент не очищает лишнее — он не знает, чем пользуется конкретный фронтенд, соответственно нужна либо ручная выборка нужных данных, либо в состояние приложения попадают лишние данные, занимающие процессорное время и память в большинстве подходов генерируется монолитный API-клиент, в котором не будет работать tree-shaking или lazy loading. Если на главной странице нужен только 1 эндпоинт — загрузятся и все остальные, а если в конкретном проекте используется только часть эндпоинтов — то ненужные все равно попадут в бандл Все, что делает сгенерированный по OpenAPI JS-клиент, — проверяет, что он сгенерирован по этой спецификации. В нем нет знания о нуждах фронтенда-потребителя или гарантии соответствия данным, реально возвращаемым бэкендом. Учитывая большие требования к организационным моментам и инфраструктуре, этот подход довольно неэффективен. Подход 2: ручное описание схемы на фронтенде Если проект перерос MVP, но не дорос до энтерпрайза, то, как правило, используют ручной подход. В OSS огромное количество библиотек для валидации, которые в основном проектировались для форм, но могут применяться и для API. Например, при использовании Zod / Valibot схемы выглядят так: import * as z from 'zod'; const userSchema = z.object({ id: z.string(), name: z.string(), age: z.number().optional(), }); type TypeUser = z.infer<typeof userSchema>;sourcecraft.dev В целом пользоваться этим подходом довольно удобно, но есть и нюансы: не все библиотеки очищают лишние параметры и логируют лишнее размер библиотек, как правило, намного больше, чем необходимо для проверки API-контрактов в проекте теперь существуют типы, написанные вручную и выведенные из валидаторов с помощью infer, что может создавать путаницу вывод типов из схемы увеличивает время прохождения typecheck-проверок, а также ненадежно работает в IDE написать схему вручную все равно немного сложнее и дольше, чем TS-тип Проблемы здесь не выглядят серьезными, но лучше все же использовать специально созданные для валидации API инструменты. Подход 3: генерация валидаторов из TS-типов Это не новый подход — в OSS давно существуют ts-interface-builder и typia. Одни библиотеки создают отдельные файлы с валидаторами, другие — непосредственно модифицируют код с помощью плагина для бандлера. Однако функционал не всегда соответствует именно нуждам проверки API-запросов в плане очистки лишних параметров, логирования, набора поддерживаемых конструкций TS AST и размера. А также итоговые схемы не всегда сериализуемы, то есть их будет сложно использовать с альтернативными библиотеками проверки или выводить в документацию. Здесь и вступает в игру api-contract-check — специализированная библиотека именно для валидации API-запросов. Один из ее модулей работает в пайплайне сборки, создавая схемы для валидации из TS-типов: import path from 'node:path'; import { createValidators } from '@arfa-tools/generator/api-contract-check/builder'; const { result, deps } = createValidators( [path.resolve('src/api/getUser.ts')], { tsconfigPath: 'tsconfig.json', requestDeclaration: 'TypeRequest', responseDeclaration: 'TypeResponse', getDegraded: (names) => console.log(names), } ); // { request: { id: 'string' }, response: { name: 'string' } } console.log(result[0].schema)sourcecraft.dev Итоговая схема — это обычный JS-объект, с которым можно работать как угодно: сериализовать через JSON.stringify и сохранить в отдельный файл, который будет лениво загружаться при вызове преобразовать в схему для Zod / аналога использовать непосредственно в коде вызова вместо отдельных файлов выводить в документацию и тому подобное В любом случае — это уже работа с обычным JS-объектом. Всю сложную работу по резолвингу импортов, сбору типов и конвертации TS AST в JS api-contract-check уже выполнил. Для работы с AST используется довольно производительный алгоритм — syntax interpreter, который по бенчмаркам производительнее ts.createProgram / oxc-parser / SWC и для файла с графом из 100 импортов выполняется за ~2 мс (вне зависимости от того, «холодный» это запуск или «горячий»). Второй модуль работает в рантайме, выполняя валидацию и очистку, при этом весит меньше 1 килобайта в сжатом виде. Если внешний интерфейс для вызова API сделать таким: import type { TypeRequest, TypeResponse } from './api/getUser'; import { validator } from './validators/api/getUser'; const user = await request<TypeRequest, TypeResponse>({ url: '/api/getUser', request: { id: 'user-1' }, validator, });sourcecraft.dev то внутри request достаточно вызвать соответствующие функции для валидации: import { check, type TypeRootSchema } from '@arfa-tools/generator/api-contract-check/checker'; async function request<TRequest, TResponse>({ url, validator, request }: { url: string; request: TRequest; validator: TypeRootSchema; }): Promise<TResponse> { const requestErrors = check({ schema: validator, type: 'request', value: request, getExtraneous: console.warn }); if (requestErrors) { throw new Error(Request validation failed:\n${requestErrors.join('\n')}); } const response = await (await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), })).json(); const responseErrors = check({ schema: validator, type: 'response', value: response, getExtraneous: console.warn }); if (responseErrors) { throw new Error(Response validation failed:\n${responseErrors.join('\n')}); } return response as TResponse; }sourcecraft.dev Все лишнее будет очищено и залогировано, а необходимое — проверено на соответствие TS-типам. Скорость работы валидации высокая — это просто проход по объекту со сравнением, но typia, разумеется, будет немного быстрее, так как не использует схемы. Если вы решили преобразовывать схемы для сторонних валидаторов, то нужно использовать их встроенные методы. Почему третий подход наиболее универсален При проверке именно API-запросов практически нет смысла проверять формат email или длину строк, поэтому схемам достаточно обеспечивать соответствие данных TS-типам, так как детальные проверки выполняются на бэкенде и на уровне БД. При этом в формах как раз можно использовать Zod-like валидаторы для оптимистичной проверки (хотя на моей практике было достаточно вручную написанных проверок без сторонних библиотек). Самое важное, что в третьем подходе остается единственный источник правды — нативный TS-тип только с нужными конкретному фронтенду полями, а приведение реальных данных в соответствие и поиск расхождений автоматизированы, находятся в request-функции и не дают некорректным данным попадать в приложение, вызывая непредсказуемые ошибки. Есть, разумеется, и недостаток — необходимо вручную синхронизировать типы в проекте с OpenAPI или бэкендерами, но это не блокирует параллельную разработку фронтенда и бэкенда, не создает существенных конфликтов при мердже и не требует большой инфраструктуры, как в первом подходе. При этом схема подходит и для небольших проектов, добавляя минимум ручной работы и позволяя отлавливать ошибки синхронизации на раннем этапе. api-contract-check входит в состав ARFA Generator через PluginValidators, упрощая интеграцию в процесс сборки, но можно использовать и отдельно для кастомных пайплайнов — например, в подходе с генерацией из OpenAPI библиотека может создавать схемы валидации, так как поддерживает in-memory резолвинг без физически сохраненных файлов. На этом все, остальное можно посмотреть в репозитории и документации, а я пойду оформлять в OpenSource другие свои библиотеки — осталось еще много интересных и много лет используемых, при этом не теряющих актуальности.

  5. Reddit · · r/typescript · u/jhnam88 · samchon/ttsc

    TTSC, TS-NODE alternative, TypeScript Runtime with Type Checking

    10 points · 2 comments
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by samchon now

No trending awards yet

None of the repositories samchon owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.