การใช้งาน JSON Schema บนพื้นฐาน TypeScript และชุดเครื่องมือพัฒนา
(github.com/imhonglu)โปรเจกต์นี้เริ่มต้นจากความตั้งใจว่า “มาลองสร้างไลบรารีที่ type-safe ในแบบที่เราชอบกันเถอะ”
โปรเจกต์นี้เริ่มจากการทำ implementation ของ JSON Schema แบบ type-safe และขยายต่อไปเป็นเครื่องมือต่าง ๆ ที่จำเป็นในกระบวนการพัฒนาอย่างเป็นธรรมชาติ
ตอนนี้ผมขอปิดเป็นหมุดหมายระยะแรกไว้ก่อนเพื่อใช้ในการหางานครับ
หลักการของโปรเจกต์
พัฒนาโดยยึดหลักสำคัญดังต่อไปนี้:
- ใช้ระบบ type ที่เข้มงวด
- รักษาการพึ่งพาภายนอกให้น้อยที่สุด
- ออกแบบระบบ type ที่นำกลับมาใช้ซ้ำได้
- จัดทำเอกสาร API
- รักษาระดับ test coverage ให้สูง
- เขียนด้วย TypeScript ล้วน
ไลบรารี
@imhonglu/json-schema
เป็น implementation ของ TypeScript ที่สอดคล้องกับสเปก JSON Schema draft 2020-12
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
- ตรวจสอบความถูกต้องผ่าน
JSON-Schema-Test-Suite - type ของ keyword ที่ใช้งานได้จะถูกอนุมานโดยอัตโนมัติตามการกำหนด schema
import { Schema, SchemaDefinition } from "@imhonglu/json-schema";
export const Address = new Schema({
type: "object",
properties: {
street: { type: "string" },
city: { type: "string" },
zip: { type: "string" },
},
required: ["street"] as const,
});
export type Address = SchemaDefinition.Instance<typeof Address>;
// {
// street: string;
// city?: string;
// zip?: string;
// }
@imhonglu/format
เป็นโปรเจกต์ที่เริ่มขึ้นเพื่อทำ implementation ของ keyword format ใน JSON Schema
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
- implementation ตามสเปก RFC
- ตรวจสอบความถูกต้องบนพื้นฐาน
JSON-Schema-Test-Suite - มีอินเทอร์เฟซที่คล้ายกับ native
JSONAPI
import { FullTime } from '@imhonglu/format';
const time = FullTime.parse('00:00:00.000Z');
// { hour: 0, minute: 0, second: 0, secfrac: '.000', offset: undefined }
console.log(time.toString()); // '00:00:00.000Z'
console.log(JSON.stringify(time)); // '"00:00:00.000Z"'
const result = FullTime.safeParse('invalid');
if (!result.ok) {
console.error(result.error);
}
@imhonglu/pattern-builder
เป็น regex builder ที่สร้างขึ้นเพื่อเพิ่มความอ่านง่ายของ regular expression ระหว่างการทำ implementation ไวยากรณ์ ABNF ตามสเปก RFC
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
import { characterSet, concat, hexDigit } from "@imhonglu/pattern-builder";
// pct-encoded = "%" HEXDIG HEXDIG
export const pctEncoded = concat(
"%",
hexDigit.clone().exact(2),
);
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
export const unreserved = characterSet(
alpha,
digit,
/[\-._~]/,
);
@imhonglu/type-guard
เป็นไลบรารี type guard ที่สร้างขึ้นเพื่อทำให้การเขียน type guard อ่านง่ายขึ้น
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
- implementation แบบอิง Proxy เพื่อลด overhead ให้น้อยที่สุด
- รองรับ keyword
not
import { composeGuards } from "@imhonglu/type-guard";
const is = composeGuards({
string: (value: unknown): value is string => typeof value === "string",
number: (value: unknown): value is number => typeof value === "number"
});
is.string("hello"); // true
is.not.string(42); // true
let value: string | number | undefined;
if (is.number(value)) {
value.toFixed(2); // 'value' is number
}
if (is.not.number(value)) {
value.toFixed(2); // error: Property 'toFixed' does not exist on type 'undefined'.
}
@imhonglu/type-object
เป็นไลบรารี wrapper แบบ type-safe สำหรับ Object API ของ native โดยให้ type ที่ใกล้เคียงกับพฤติกรรมของ native
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
import * as TypeObject from '@imhonglu/type-object';
const data = { a: 1, b: 2, c: 3 };
for (const key of TypeObject.keys(data)) {
// key: "a" | "b" | "c"
console.log(data[key]); // number
}
const string = 'hello';
for (const index of TypeObject.keys(string)) {
// index: number & keyof string
console.log(string[index]); // string
}
@imhonglu/toolkit
เป็นชุดรวม utility type และ utility function ที่ใช้งานอยู่ภายในโปรเจกต์
- ที่เก็บโค้ด: https://github.com/imhonglu/new-wheels/…
import type { Fn } from '@imhonglu/toolkit';
// มี type alias สำหรับชนิดฟังก์ชัน '(...args: any[]) => any'
Fn.Callable // (...args: any[]) => any
// สามารถกำหนดเฉพาะชนิดของอาร์กิวเมนต์ผ่าน generic ได้
Fn.Callable<{ args: [number, number] }> // (...args: [number, number]) => any
// สามารถกำหนดเฉพาะชนิดคืนค่าผ่าน generic ได้
Fn.Callable<{ return: string }> // (...args: any[]) => string
// สามารถกำหนดทั้งชนิดของอาร์กิวเมนต์และชนิดคืนค่าผ่าน generic ได้
Fn.Callable<{ args: [number, number], return: string }> // (...args: [number, number]) => string
แผนถัดไปและการหางาน
ขั้นต่อไปของโปรเจกต์ที่กำลังดำเนินอยู่คือทำ implementation ของสเปก JSON Schema ให้เสร็จสมบูรณ์
และผมก็อยากลองเขียน backend framework ดูครับ
ตอนนี้กำลังหางานอยู่ ฝากติดตามกันด้วยนะครับ
ขอบคุณที่อ่านครับ
ขอให้เป็นวันที่ดีครับ!
2 ความคิดเห็น
ฝั่งนี้มีตัวเด่นอย่าง zod อยู่แล้ว เลยใช้ตัวนั้นในโปรดักต์กันอยู่ แต่ก็น่าสนใจดีนะ
โปรเจ็กต์อย่าง ajv, typia, zod เป็นต้น เป็นโปรเจ็กต์ที่มีอยู่แล้วและผมเองก็ติดตามด้วยความสนใจเช่นกัน
ในกรณีของ
safeParseของ@imhonglu/formatก็เป็นฟีเจอร์ที่ได้รับอิทธิพลมาจาก zod API เช่นกันขอบคุณที่ให้ความสนใจ!