Skip to content

Tealina 通过少量约定和 Typescript 基本特性, 实现端到端类型. 这里使用最少代码助你快速理解其原理:

类型提取流程图

type-flow

批量导出 API

以 key value 结构批量导出 API 定义, 这带来两个好处:

  1. 每个 api 目录只需定义一次路由
  2. 方便后续映射类型
ts
/// [api-dir/index.ts]
export default {
  post: import("./post/index.js"),
}

// [api-dir/post/index.ts]
export default {
  "category/create": import("./category/create.js"),
}
/// [api-dir/index.ts]
export default {
  post: import("./post/index.js"),
}

// [api-dir/post/index.ts]
export default {
  "category/create": import("./category/create.js"),
}

类型别名

基于框架的 Handler 函数, 封装一个类型别名:
HandlerType<输入, 输出, 请求头, ...其他>

ts
interface RawPayload {
  body?: unknown
  params?: unknown
  query?: unknown
}

type HandlerType<T extends RawPayload, Treponse, Theaders> = (req, res) => any

// api-v1/post/category/create.ts

//声明API函数类型 👇
type ApiType = HandlerType<{ body: Pure.CategoryCreateInput }, Pure.Category>

const handler: ApiType = (req, res) => {
  ///....
}

export default handler
interface RawPayload {
  body?: unknown
  params?: unknown
  query?: unknown
}

type HandlerType<T extends RawPayload, Treponse, Theaders> = (req, res) => any

// api-v1/post/category/create.ts

//声明API函数类型 👇
type ApiType = HandlerType<{ body: Pure.CategoryCreateInput }, Pure.Category>

const handler: ApiType = (req, res) => {
  ///....
}

export default handler

类型提取和重映射

使用 infer 语法, 提取 API 信息

ts
import apis from "../src/api-v1/index.ts"

type ExtractApiType<T> = T extends HandlerType<
  infer Payload,
  infer Response,
  infer Headers
>
  ? Payload & { response: Response; headers: Headers }
  : never

type RawApis = typeof apis
export type ApiTypesRecord = {
  [Method in keyof RawApis]: ExtractApiType<Awaited<RawApis[Method]>["default"]>
}
import apis from "../src/api-v1/index.ts"

type ExtractApiType<T> = T extends HandlerType<
  infer Payload,
  infer Response,
  infer Headers
>
  ? Payload & { response: Response; headers: Headers }
  : never

type RawApis = typeof apis
export type ApiTypesRecord = {
  [Method in keyof RawApis]: ExtractApiType<Awaited<RawApis[Method]>["default"]>
}

ApiTypesRecord 会变成这样

ts
type ApiTypesRecord = {
  post: {
    "category/create": {
      body: Pure.CategoryCreateInput
      response: Pure.Category
    }
  }
}
type ApiTypesRecord = {
  post: {
    "category/create": {
      body: Pure.CategoryCreateInput
      response: Pure.Category
    }
  }
}

类型共享给前端

把类型暴露给前端, 而且只是类型, 运用 node 的包管理特性, 在 server/package.json 定义 exports 类型声明, 导出 API 类型

json
// server/package.json
{
  "exports": {
    "./api/v1": "./types/api-v1.d.ts"
  }
}
// server/package.json
{
  "exports": {
    "./api/v1": "./types/api-v1.d.ts"
  }
}

在前端 web/packages.json 添加 server 为 devDependencies

json
// web/package.json
{
  "devDependencies": {
    "server": "link:../server"
  }
}
// web/package.json
{
  "devDependencies": {
    "server": "link:../server"
  }
}

WARNING

在前端执行 build 的时候, Typescript 会使用 web/tsconfig.json 中的约束规则检查后端的 ts 代码, 原因是 tsc 检查只是跳过.d.ts 文件, 不跳过.ts 文件. 所以请确保前后端约束规则保持一致.

封装请求函数

用后端导出的 API 类型, 封装一个 request 对象, 实现写代码的时候用的是实时类型, 利用 proxy 特性, 将所有请求交给 axios.request 处理.

ts
import axios from "axios"
import { ApiTypesRecord } from "server/api/v1"
import { MakeReqType, createReq } from "./createReq"

const instance = axios.create({
  baseURL: "/api/v1/",
})
instance.interceptors.response.use((v) => v.data)

export const req = createReq<MakeReqType<ApiTypesRecord>>(instance)
import axios from "axios"
import { ApiTypesRecord } from "server/api/v1"
import { MakeReqType, createReq } from "./createReq"

const instance = axios.create({
  baseURL: "/api/v1/",
})
instance.interceptors.response.use((v) => v.data)

export const req = createReq<MakeReqType<ApiTypesRecord>>(instance)
ts
/**
 * 返回一个代理对象内部指向`axiosInstance`.
 * @param axiosInstance
 */
const createReq = <T extends ApiShape>(axiosInstance: AxiosInstance) =>
  new Proxy({} as T, {
    get:
      (_target, method: string) =>
      (url: string, ...rest: DynamicParmasType) =>
        axiosInstance.request({
          method,
          url,
          ...transformPayload(url, rest),
        }),
  })
/**
 * 返回一个代理对象内部指向`axiosInstance`.
 * @param axiosInstance
 */
const createReq = <T extends ApiShape>(axiosInstance: AxiosInstance) =>
  new Proxy({} as T, {
    get:
      (_target, method: string) =>
      (url: string, ...rest: DynamicParmasType) =>
        axiosInstance.request({
          method,
          url,
          ...transformPayload(url, rest),
        }),
  })

使用 req 调用 API, 全程带智能提示

ts
//web/src/some-file.ts
import { req } from "api/req.ts"

req.post("category/create", {
  body: {
    categoryName: "Books",
    description: "Desc...",
  },
})
//web/src/some-file.ts
import { req } from "api/req.ts"

req.post("category/create", {
  body: {
    categoryName: "Books",
    description: "Desc...",
  },
})

类型延迟

如果遇到后端类型有更新, 前端没反应过来, 这种情况是因为 Typescript 语言服务有缓存, 两种解决方法:

  1. 定位到变量, f12 跳转到定义, 触发类型刷新.
  2. 手动重启 TS 服务, 以 VS code 为例, Ctrl + Shift + P, 找到: Restart TS Server 并执行