初窥门径:解锁 TypeScript 的 as const 与 satisfies 的使用

326 阅读2分钟

先叠个甲: 作者不精通 TypeScript,对 JavaScript、Vue、React 完全没有深入了解。

背景

为了方便理解 as const 与 satisfies,下面假设了一个特定的需求:为 Vue 或者 React 项目的 Router 模块提供关于嵌套路由 path 字段的类型提取工具:

如上图所示,RoutePath的类型就是我希望得到的最终产物。

注意:satisfies 特性在 TypeScript 4.9 及以上版本中才可用,因此讨论基于该版本之后。

代码

以下代码来自我学习 React 时的项目。

src/router.tsx

import type { RouteObject } from 'react-router-dom'
import { Navigate } from 'react-router-dom'
import App from './App'
import Buttons from './pages/Buttons'
import Error from './pages/Error'
import Game from './pages/Game'
import Immer from './pages/Immer'
import ProductList from './pages/ProductList'
import ReducerContext from './pages/ReducerContext'

export const routes = [
  {
    path: '/' as const,
    element: <App />,
    errorElement: <Error />,
    children: [
      {
        path: '' as const,
        element: <Navigate to="buttons" replace></Navigate>,
      },
      {
        path: 'buttons' as const,
        element: <Buttons />,
      },
      {
        path: 'game' as const,
        element: <Game />,
      },
      {
        path: 'product-list' as const,
        element: <ProductList />,
      },
      {
        path: 'immer' as const,
        element: <Immer />,
      },
      {
        path: 'reducer-context' as const,
        element: <ReducerContext />,
      },
    ],
  },
] satisfies RouteObject[]

interface Route {
  path: string
  children?: Route[]

  [k: string]: any
}

type JoinPath<Parent extends string, Child extends string> = Parent extends '/' ? `/${Child}` : Child extends `/${string}` ? `${Parent}${Child}` : `${Parent}/${Child}`

type ExtractRoutePath<T extends Route[], ParentPath extends string = ''> = T extends (infer R)[]
  ? R extends Route
    ? R['path'] extends string
      ? R['children'] extends Route[]
        ? JoinPath<ParentPath, R['path']> | ExtractRoutePath<R['children'], JoinPath<ParentPath, R['path']>>
        : JoinPath<ParentPath, R['path']>
      : never
    : never
  : never

// Expect: type RoutePath = "/" | "/buttons" | "/game" | "/product-list" | "/immer" | "/reducer-context"
export type RoutePath = ExtractRoutePath<typeof routes>

解释

下面将解释 as constsatisfies 的作用,然后简单解释一下 ExtractRoutePath 这个工具类型的基本用法,如前文所说,我并不精通 TypeScript,因此我无法解释其中的原理。

as const:将数据设为常量

"你只能是一只好猴子"

附加 as const 后,字面量(如对象、数组、字符串等)会被标记为只读常量,TypeScript 会推断出最窄的字面量类型。

const status = 'success' as const;
// status 的类型被推断为 'success',而不是 string

在一个数组中,成员被 as const 断言后,数组的类型推断也会变窄,如下所示。

satisfies: 在类型验证中保持灵活

"可你只需要像他,不需要是他"

有时,我们需要验证对象字面量是否符合某个接口,但不想丢失对象的额外属性。satisfies 允许我们保留对象的完整类型信息,同时验证结构符合要求。

区别于 as 关键字的类型断言,satisfies 不改变对象的类型。因此,它在你需要定义一些 "config" 的时候,可以增强类型安全性和智能提示:

ExtractRoutePath: 提取嵌套路由的类型

"关关难过关关过"

类型推断、条件类型、联合类型对重复类型的合并等特性,可以组合成一个递归提取嵌套路由定义的工具:

type JoinPath<Parent extends string, Child extends string> = 
  Parent extends '/' 
    ? `/${Child}` // 如果 Parent 是根路径 '/', 则直接返回 '/Child'
    : Child extends `/${string}` 
      ? `${Parent}${Child}` // 如果 Child 已经是以 '/' 开头的路径,则直接连接 Parent 和 Child
      : `${Parent}/${Child}` // 否则,在 Parent 和 Child 之间加上 '/' 进行连接

type ExtractRoutePath<T extends Route[], ParentPath extends string = ''> = 
  T extends (infer R)[] 
    ? R extends Route 
      ? R['path'] extends string 
        ? R['children'] extends Route[] 
          ? JoinPath<ParentPath, R['path']> | ExtractRoutePath<R['children'], JoinPath<ParentPath, R['path']>> 
            // 如果 R 有子路由,则递归调用 ExtractRoutePath 处理子路由,并将当前路径与子路径连接
          : JoinPath<ParentPath, R['path']> 
            // 如果没有子路由,则返回当前路径
        : never 
      : never 
    : never