6 คะแนน โดย honglu 2025-01-08 | 2 ความคิดเห็น | แชร์ทาง WhatsApp

โปรเจกต์นี้เริ่มต้นจากความตั้งใจว่า “มาลองสร้างไลบรารีที่ 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

[IMG] demo-1

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 JSON API
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

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

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 ที่ใช้งานอยู่ภายในโปรเจกต์

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 ความคิดเห็น

 
jjpark78 2025-01-09

ฝั่งนี้มีตัวเด่นอย่าง zod อยู่แล้ว เลยใช้ตัวนั้นในโปรดักต์กันอยู่ แต่ก็น่าสนใจดีนะ

 
honglu 2025-01-09

โปรเจ็กต์อย่าง ajv, typia, zod เป็นต้น เป็นโปรเจ็กต์ที่มีอยู่แล้วและผมเองก็ติดตามด้วยความสนใจเช่นกัน

ในกรณีของ safeParse ของ @imhonglu/format ก็เป็นฟีเจอร์ที่ได้รับอิทธิพลมาจาก zod API เช่นกัน

ขอบคุณที่ให้ความสนใจ!