在TypeScript中使用infer来解包嵌套类型

图片

最近在做一个项目时,我发现了一些有趣的东西:TypeScript 的 infer 关键字可以推断出很多东西,它提供了一种更直接的方式来实现对嵌套类型的解包。

这个想是由 crosswalk 库激发的,它可以帮助你构建和使用类型安全的 REST API,使用infer 也可以在这种上下文中工作,但是当我们看 OpenAPI Schema 时,差异会更加明显。

假设你的API允许你创建一个用户(它也允许你GET一个用户,但我只在这里显示POST,因为这已经很详细了)。

{
  "openapi": "3.0.3",
  "info": { "title": "Users API", "version": "0.1" },
  "paths": {
    "/users": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Newly-created User",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateUserRequest": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "age": { "type": "number" }
        },
        "required": [ "name", "age" ]
      },
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "age": { "type": "number" }
        },
        "required": [ "id", "name", "age" ]
      }
    }
  }
}

你可以通过 openapi-typescript 运行这个来生成 TypeScript 类型:

/** This file was auto-generated by openapi-typescript. */

export interface paths {
  "/users": {
    post: {
      requestBody: {
        content: {
          "application/json": components["schemas"]["CreateUserRequest"];
        };
      };
      responses: {
        /** @description Newly-created User */
        200: {
          content: {
            "application/json": components["schemas"]["User"];
          };
        };
      };
    };
    // also get, etc.
  };
}

export interface components {
  schemas: {
    CreateUserRequest: {
      name: string;
      age: number;
    };
    User: {
      id: string;
      name: string;
      age: number;
    };
  };
}


通过  path  结构访问这些类型涉及到一整串索引:

type UserResponse = paths["/users"]["post"]["responses"][200]["content"]["application/json"];


如果你想做一个通用的 POST 方法,你很快就会遇到一些错误:

declare function post<Path extends keyof paths>(
  endpoint: Path
): Promise<paths[Path]["post"]["responses"][200]["content"]["application/json"]>;
//         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"application/json"' cannot be used to index type 'paths[Path]["post"]["responses"][200]["content"]'. (2536)


这个错误是有道理的:TypeScript没有理由相信这个深层索引操作序列将工作在任意类型上。

你可以使用 infer 来提取你想要的结构:

type HttpVerb = 'get' | 'post' | 'patch' | 'delete' | 'update';
type ResponseForMethod<Path extends keyof paths, Verb extends HttpVerb> =
  paths[Path] extends Record<Verb, {
    responses: {
      200: {
        content: {
          'application/json': infer ResponseType,  // <-- the "infer"
        }
      }
    }
  }> ? ResponseType : never;

declare function post<Path extends keyof paths>(
  endpoint: Path
): Promise<ResponseForMethod<Path, 'post'>>;

const response = post('/users');
//    ^? const response: Promise<{ id: string; name: string; age: number; }>


令我惊讶(并印象深刻)的是 infer 可以直接看到 Record 帮助器,后者是映射类型的包装器。

如果端点不支持 POST 请求,你将得到一个 never 类型返回,一个类型错误会更好,但希望 never 将足以提醒调用者,有些地方出错了。

我以前的的话会建议用这个帮助器链代替:

type LooseKey<T, K> = T[K & keyof T];
type LooseKey2<T, K1, K2> = LooseKey<LooseKey<T, K1>, K2>;
type LooseKey3<T, K1, K2, K3> = LooseKey<LooseKey2<T, K1, K2>, K3>;
type LooseKey4<T, K1, K2, K3, K4> = LooseKey<LooseKey3<T, K1, K2, K3>, K4>;
type LooseKey5<T, K1, K2, K3, K4, K5> = LooseKey<LooseKey4<T, K1, K2, K3, K4>, K5>;

type ResponseForMethod<Path extends keyof paths, Verb extends HttpVerb> =
  LooseKey5<paths[Path], Verb, 'responses', 200, 'content', 'application/json'>;


但是与这个交集形式相比,infer 方法更直接、更易读、更紧密地匹配数据的布局。而且如果您需要从一个类型中提取多个信息,那么如何做就很清楚了。这是一个不错的胜利。

“把你拥有的和 TypeScript 想要的相互交叉”仍然是一个有用的技术吗?当你把一个类型参数传递给另一个泛型类型时,你有时需要写  & string :

// see previous post
type ExtractRouteParams<Path extends string> = ...;
// e.g. ExtractRouteParams<'/users/:userId'> = {userId: string}

class ApiWrapper<API> {
  apiGet<Path extends keyof API>(
    path: Path,
    queryParams: ExtractRouteParams<Path>,
    //                              ~~~~
    // Type 'Path' does not satisfy the constraint 'string'.
    //   Type 'keyof API' is not assignable to type 'string'.
    //     Type 'string | number | symbol' is not assignable to type 'string'.
    //       Type 'number' is not assignable to type 'string'. (2344)
  ): Promise<GetResponseForMethod<API, Path>>;
}

查看TypeScript Splits the Atom中 ExtractRouteParams 的定义,问题是我们期望 keyof API 是 string 的子类型,但严格来说 TypeScript 知道的是它是 PropertyKey 的子类型,也就是 string | number | symbol ,没有什么阻止 API 成为 string[],例如,在这种情况下 keyof API = number,因为 ExtractRouteParams 期望一个 string

最好的解决方法是告诉 TypeScript  API 应该只有 string 键,但我不知道如何做到这一点:写ApiWrapper<API extends Record<string, any>> 会导致同样的错误。

在这种情况下,使用交集来消除错误仍然有意义:

class ApiWrapper<API> {
  apiGet<Path extends keyof API>(
    path: Path,
    queryParams: ExtractRouteParams<Path & string>,  // ok
  ): Promise<GetResponseForMethod<API, Path>>;
}


当你使用嵌套对象类型时,请记住,你可以使用 infer 从它们内部提取特定的类型。

 欢迎关注公众号:文本魔术,了解更多

 

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue 类型注释只能在 TypeScript 文件使用的原因是因为 TypeScript 是一种静态类型检查的编程语言,可以通过类型注释来提供代码的类型信息,从而在编译期间进行类型检查,减少运行时的错误。而 Vue 是一个基于 JavaScript 的框架,JavaScript 是一种动态类型的编程语言,没有编译阶段,所以无法进行静态类型检查。 然而,由于 Vue 在项目使用了许多特定的语法和生命周期,TypeScript 提供了一种使用类型注释来增强 Vue 代码的可读性和可维护性的方式。通过在 Vue 组件文件使用 TypeScript 的扩展语法,我们可以将类型注释应用于 Vue 组件的 prop、data、computed 等属性上。这样在开发过程,编辑器可以根据类型注释提供代码补全、错误检查等功能,从而提高开发效率和代码质量。 需要注意的是,为了使用 Vue 类型注释,我们需要在 TypeScript 文件引入 Vue 的类型声明文件,以使 TypeScript 理解 Vue 的特定语法和类型信息。同时,我们还需要在项目配置添加相应的 TypeScript 相关配置,以确保 TypeScript 在编译过程正确解析和处理 Vue 组件类型注释。 总之,Vue 类型注释只能在 TypeScript 文件使用,是因为 TypeScript 提供了一种增强 Vue 代码的方式,通过类型注释来提供静态类型检查和代码补全等功能,从而提高项目的代码质量和开发效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值