Skip to content

Tealina implements end-to-end typing through a few conventions and the basic features of Typescript, Here is the least amount of code is used to help you quickly understand its principle:

Type extraction flowchart

type-flow

Batch export APIs

Batch export of APIs using the key value structure brings two benefits:

  1. Each API directory only needs to define a route once
  2. Convenient for subsequent mapping types
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'),
}

Type alias

A framework based handler function that encapsulates a type alias:
HandlerType<Input, Output, Header, ...Other>

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

// Declare API function type
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

// Declare API function type
type ApiType = HandlerType<{ body: Pure.CategoryCreateInput }, Pure.Category>

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

export default handler

Type extraction and remapping

Using the infer syntax to extract API information

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 will become like this

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

Type sharing with frontend

Using the package management feature of Node, expose the type to the frontend, Define the exports type declaration in server/package.json and export API types

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

Add server as devDependencies in the frontend web/packages.json

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

WARNING

When executing build on the frontend, Typescript will use the constraint rules in web/tsconfig.json to check the TS code on the backend. The reason is that the TSC check only skips the .d.ts file, not the .ts file. Therefore, Ensure that the constraint rules on the frontend and backend are consistent

Encapsulation request function

Encapsulate a request object using the API type exported from the backend, When writing code, real-time types are used, and using the proxy feature, all requests are handed over to axios.request for processing

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
/**
 * Returns a proxy object pointing internally to ` 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),
        }),
  })
/**
 * Returns a proxy object pointing internally to ` 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),
        }),
  })

Using 'req' to call APIs with intelligent prompts throughout the process

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...',
  },
})

Type delay

If there is an update to the backend type and the frontend does not respond, This situation is because the Typescript language service has a cache, Two solutions:

  1. Locate the variable, F12 jumps to the definition, triggering a type refresh
  2. Manually restart the TS service, using VS code as an example, Ctrl+Shift+P, find: Restart TS Server and execute it