🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

引言

你好,聖誕節還有幾天就到了。今年的結尾讓我每天都感受到時間在流逝🎄

這篇關於降臨曆的文章第二篇將介紹幾個在過去兩到三年中我遇到的 TypeScript 型別的相關庫,這些庫讓我感到驚豔。我們將具體探討這些庫如何提升開發者體驗(DX),並結合實際代碼示例和我的體驗來進行解說。

1. Hono - 型別安全的輕量級 Web 框架

Hono 是由 Cloudflare 的工程師和田裕介(Yusuke Wada)所開發的高性能 Web 框架,受到越來越多的歡迎。順便提一下,這個名字的來源是「火焰」(ほのお),對於開發者來說很有日本特色。[^1]

首先讓人驚訝的是,路徑參數竟然實現了型別安全。在幾年前的框架中,路徑參數可能是 { [key: string]: string }any,現在回想起來,簡直就像石器時代一樣。

import { Hono } from 'hono'

const app = new Hono()

app.get('/authors/:name', (c) => {
  const { name: author } = c.req.param() // { name: string }
  return c.json({ author })
})

型別安全的中介軟體

在傳統的框架中,當動態為請求物件添加屬性時,TypeScript 的型別檢查會變得不太可靠,需要手動為其指定型別。而 Hono 則透過 ContextVariableMap 機制來保證型別的安全性。

import { createMiddleware } from 'hono/factory'

declare module 'hono' {
  interface ContextVariableMap {
    result: string
  }
}

const mw = createMiddleware(async (c, next) => {
  c.set('result', 'some values') // ('result', string)
  await next()
})
app.use(mw)

app.get('/', (c) => {
  const result = c.get('result') // ('result') => string
  return c.json({ result })
})

型別安全的 RPC

在 TypeScript 中提到型別安全的 RPC,大家可能會想到 tRPC,而 Hono 通過從伺服器定義導出 AppType 來支持型別安全的 RPC。

import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const schema = z.object({
  id: z.number(),
  title: z.string()
})
const app = new Hono().basePath('/v1')

const routes = app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({
    message: `${data.id.toString()} is ${data.title}`
  })
})
type AppType = typeof routes

從客戶端可以利用 AppType 來構建型別安全的 API 請求。

import { hc } from 'hono/client'

const client = hc<AppType>("http://localhost:8787")
// 型別安全的 `v1.posts.$post`
const res = await client.v1.posts.$post({
  // 型別安全的 `json: { id: number; title: string; }`
  json: { id: 123, title: "Hello" },
})
const data = await res.json()
console.log(data.message) // string

Elysia - 從型別生成 OpenAPI

同樣概念的框架還有 Elysia(官方的日文讀音為「エリシア」),它是專為 Bun 開發的。雖然與 Hono 直接無關,但最近 Bun 被 Anthropic 收購的消息引起了廣泛關注。[^2]

Elysia 提供與 Hono 相等或更高的型別安全性,並且和 Bun 的快速性結合,實現了更高的性能,未來非常值得關注。

import { Elysia } from 'elysia'
import { openapi, fromTypes } from '@elysiajs/openapi'

export const app = new Elysia()
  .use(
    openapi({
      // 這樣就可以從型別自動生成 OpenAPI 文檔
      references: fromTypes(),
    })
  )
  .get('/', { test: 'hello' as const })
  .post('/json', ({ body, status }) => body, {
    body: t.Object({
      hello: t.String(),
    }),
  })
  .listen(3000)

2. ArkType - 直觀且快速的驗證

提到運行時的數據類型檢查(驗證),Zod 是最著名的選擇,但新興的 ArkType 在這方面取得了驚人的進展。

  • 像 TypeScript 語法一樣直觀的寫法
  • 相比 Zod 高出約 20 倍的性能
import { z } from 'zod'

const User = z.object({
  name: z.string(),
  age: z.number().min(0).max(120),
  email: z.string().email(),
  isActive: z.boolean().optional(),
})
type User = z.infer<typeof User>

相比 Zod 的寫法,ArkType 的寫法更接近 TypeScript 的型別定義語法,直觀性更強。數值範圍的指定也可以用字串自然表達。

import { type } from 'arktype'

const User = type({
  name: 'string',
  age: '0 <= number.integer <= 120',
  email: 'string.email',
  'isActive?': 'boolean',
})
type User = typeof User.infer

ArkRegex - 型別安全的正規表達式

另外,最近 ArkType 新增了 ArkRegex 功能,這標誌著型別安全的正則表達式時代的到來!

import { regex } from 'arktype'

const Email = regex('^(?<name>\\w+)@(?<domain>\\w+\\.\\w+)$')

const parts = Email.exec('[email protected]')
if (parts?.groups) {
  const { name, // string
    domain, // `${string}.${string}`
  } = parts.groups
}

3. Kysely - 型別安全的資料庫操作

最近我接觸到了 Android 開發,並在 Room 的編譯時 SQL 和架構驗證中感到驚訝。Kysely 是一個輕量級的 SQL 包裝器,而非 ORM,但它為 TypeScript 帶來了類似的體驗。

首先,我們需要用型別定義資料庫的架構。

import { Generated } from 'kysely'

interface Database {
  user: {
    id: Generated<number>
    name: string
    gender: 'man' | 'woman' | 'other'
    age: number
  }
  post: {
    id: Generated<number>
    author_id: number // 外鍵 -> user
    text: string
  }
}

這樣就能用 SQL 類似的直觀寫法進行型別安全的資料庫操作。

import SQLite from 'better-sqlite3'
import { Kysely, SqliteDialect } from 'kysely'

const dialect = new SqliteDialect({
  database: new SQLite(':memory:'),
})
const db = new Kysely<Database>({ dialect })

// {
//   text: string;
//   id: number;
//   author: string;
// }[]
const posts = await db
  .selectFrom('user')
  .where('id', '=', 123)
  .innerJoin('post', 'author_id', 'user.id')
  .select(['post.id', 'name as author', 'text'])
  .execute()

不必擔心欄位名稱的拼寫錯誤,也能輕鬆進行架構的添加和查詢的重構,利用編譯時的型別檢查實現了優秀的 DX。

4. TanStack Router - 型別安全的路由

今年 Next.js 針對型別路由進行了整合,但仍然是個不完整的解決方案,對於 searchParams 仍需使用像 nuqs 的庫或自己補全。

// src/app/posts/[page].tsx
async function PostsPage({ params, searchParams }) {
  // page: string
  const { page } = await params
  // sortParam: string | string[] | undefined
  const { sort: sortParam } = await searchParams
  const sort = ['newest', 'oldest', 'popular'].includes(sortParam) ? sortParam as 'newest' | 'oldest' | 'popular' : 'newest'
}

TanStack Router 中,不僅僅是 URL 的路徑參數,連 searchParams 的驗證和型別推斷也能夠保證型別安全。

// src/routes/posts/$page.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const searchSchema = z.object({
  sort: z.enum(['newest', 'oldest', 'popular']).default('newest'),
})

const Route = createFileRoute('/posts/$page')({
  validateSearch: searchSchema,
})

function PostsPage() {
  // page: string
  const { page } = Route.useParams()
  // sort: 'newest' | 'oldest' | 'popular'
  const { sort } = Route.useSearch()
}

在鏈接到頁面時可以享受型別安全帶來的好處。

import { Link } from '@tanstack/react-router'

<Link
  to="/posts/$page"
  params={{ page: '123' }}
  search={{ sort: 'popular' }}
/>

5. Wagmi 和 ABIType - Web3 的型別安全性

直到去年,我花了大約三年的時間在 Web3 相關項目的開發上。Web3 的機制是基於 Solidity 語言開發的智能合約(運行在區塊鏈上的以太坊虛擬機上),並且通常通過 ethers.js 等庫與智能合約進行 RPC 通信,實現各種功能。這樣的通信協議由 ABI(應用二進位介面)定義。

從 TypeScript 的角度看,ethers.js 中充斥著 any 型別,型別安全相距甚遠,真是可怕的世界。

contract = new Contract("0x...", abi, provider)
// result: any
const result = await contract.balanceOf('0x123')

當時部分解決這個問題的方法是使用 TypeChain 的工具,這個工具能夠從 ABI 生成 TypeScript 型別。

隨著時間的推移,Wagmi 這個優秀的庫出現了。它內部使用了一個神奇的庫 ABIType,通過從 ABI 的 JSON 定義推斷 TypeScript 型別,從而實現了型別安全的 RPC 通信。

import { Address, useReadContract } from 'wagmi'

// 通過編譯 Solidity 程式獲取
const abi = [
  {
    name: 'balanceOf',
    type: 'function',
    inputs: [{ name: 'owner', type: 'address' }],
    outputs: [{ name: 'balance', type: 'uint256' }],
    stateMutability: 'view',
  },
] as const

function TokenBalance(owner: Address) {
  // data: bigint | undefined
  const { data, isError, isLoading } = useReadContract({
    address: '0x...',
    abi,
    functionName: 'balanceOf',
    args: [owner],
  })
}

更進一步,ABI 不僅支持上述的 JSON 格式,還支持 Solidity 語言的表述。也就是說,型別級別的文法分析正在進行着⁉️

const abi = [
  'function balanceOf(address owner) view returns (uint256)',
] as const

// data: bigint | undefined
// const { data, isError, isLoading } = useReadContract({ abi, ... })

6. TypeGPU - WebGPU 的型別安全性

今年初我有機會接觸 WebGPU,但由於不熟悉,遇到了著色器程式(WGSL)和主機程式(TypeScript)之間的一致性問題。對於這樣一段簡單的程式,可能能夠快速修正錯誤,但當時的程式中包含多個 Compute Shader,實在是糟糕透頂。

// 只是字符串,使得與 TypeScript 的關聯難以理解
const shaderCode = `
  struct VertexOutput {
    @builtin(position) position: vec4f,
    @location(0) uv: vec2f,
  };

  @vertex
  fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
    // ...
  }

  @fragment
  fn fragmentMain(@location(0) uv: vec2f) -> @location(0) vec4f {
    let purple = vec4f(0.769, 0.392, 0.230, 1.0);
    let blue = vec4f(0.114, 0.847, 0.241, 1.0);
    let ratio = (uv.x + uv.y) / 2.0;
    return mix(purple, blue, ratio);
  }
`

const shaderModule = device.createShaderModule({
  code: shaderCode,
})

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vertexMain',
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fragmentMain',
    targets: [
      {
        format: presentationFormat,
      },
    ],
  },
  primitive: {
    topology: 'triangle-list',
  },
})
// ...

TypeGPU 利用 TypeScript 的型別系統來解決這些問題。然而,並不幸的是,它無法直接在型別層面解析 WGSL,而需要用 TypeScript 來編寫著色器代碼,然後通過捆綁器的插件[^3]轉換成 WGSL。

雖然也支持用 WGSL 編寫,但那樣就失去了型別檢查帶來的好處,因此我個人並不偏好。

import tgpu from 'typegpu'
import * as d from 'typegpu/data'
import * as std from 'typegpu/std'

const purple = d.vec4f(0.769, 0.392, 1.0, 1)
const blue = d.vec4f(0.114, 0.447, 0.941, 1)

const getGradientColor = (ratio: number) => {
  'use gpu'
  return std.mix(purple, blue, ratio)
}

const mainVertex = tgpu['~unstable'].vertexFn({
  in: { vertexIndex: d.builtin.vertexIndex },
  out: { outPos: d.builtin.position, uv: d.vec2f },
})(({ vertexIndex }) => {
  const pos = [d.vec2f(0.0, 0.5), d.vec2f(-0.5, -0.5), d.vec2f(0.5, -0.5)]
  const uv = [d.vec2f(0.5, 1.0), d.vec2f(0.0, 0.0), d.vec2f(1.0, 0.0)]
  return {
    outPos: d.vec4f(pos[vertexIndex], 0.0, 1.0),
    uv: uv[vertexIndex],
  }
})

const mainFragment = tgpu['~unstable'].fragmentFn({
  in: { uv: d.vec2f },
  out: d.vec4f,
})(({ uv }) => {
  return getGradientColor((uv[0] + uv[1]) / 2)
})

const root = await tgpu.init()
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

const pipeline = root['~unstable']
  .withVertex(mainVertex, {})
  .withFragment(mainFragment, { format: presentationFormat })
  .createPipeline()
// ...

在一般開發中,使用 Three.js 或 Unity 的機會不多,直接接觸 WebGPU 的機會更加稀少,但我很高興遇見 TypeGPU,就像在 Linux 核心開發中使用 Rust 一樣,感覺格外振奮🥳[^4]

總結

你覺得怎麼樣?我介紹了六個在 Web 開發中不可或缺的庫,從中介軟體、RPC、OpenAPI、驗證、資料庫操作、路由一直到 Web3 和 WebGPU 等尖端技術,TypeScript 的型別系統都在閃閃發光。

這些庫覆蓋了不同的領域,但共同的理念是在編譯時檢測更多的錯誤,以減少運行時的錯誤。如果你認同這個理念,對這些庫感興趣,不妨試試看!

目指❣️依存型❣️ 沒有什麼啦😅

最後,祝你有個美好的聖誕節✨

[^1]: Hono 網頁框架的故事,來自 Hono 的創建者
[^2]: Anthropic 購併 Bun,Claude Code 達成 10 億美元里程碑
[^3]: 構建插件 | TypeGPU
[^4]: Linus 在日本期間發布 Linux 6.19-rc1,Rust 導入實驗已完成進入下一階段 | gihyo.jp


原文出處:https://qiita.com/touka-io/items/c4104488234206efb0d5


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬9   ❤️5
347
🥈
我愛JS
📝1   💬8   ❤️2
62
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付