当我开始(和我的团队)用 TypeScript 和 Svelte(我们都讨厌的 JavaScript 和 React)重写我们的应用程序时,我遇到了一个问题:
如何安全地输入 HTTP 响应的所有可能的正文?
这对你来说有启发吗?如果没有,你很可能就是“其中之一”,呵呵。让我们暂时离题一下,以便更好地理解图片。
似乎没有人关心 HTTP 响应的“所有可能的主体”,因为我找不到为此做的任何东西(好吧,也许是 ts-fetch)。让我快速通过我的逻辑来解释为什么会这样。
没有人关心,因为人们:
只关心happy path:HTTP状态码为2xx时的响应体。
人们在其他地方手动输入它。
对于#1,我想说的是,开发人员(尤其是没有经验的开发人员)忘记了 HTTP 请求可能会失败,并且失败响应中携带的信息很可能与常规响应完全不同。
对于#2,让我们深入研究 ky 和 axios 等流行 NPM 包中发现的一个大问题。
据我所知,人们喜欢像 ky 或 axios 这样的包,因为它们的“功能”之一是它们会在不正常的 HTTP 状态代码上抛出错误。从什么时候开始可以这样了?自从从来没有。但显然人们并没有意识到这一点。人们很高兴并且满足于在不正常的响应中遇到错误。
我想人们在捕捉的时候会输入不正常的身体。多么混乱,多么有代码味道!
这是一种代码味道,因为您有效地使用 try..catch 块作为分支语句,而 try..catch 并不意味着是分支语句。
但即使你与我争论分支在 try..catch 中自然发生,还有另一个导致这种情况仍然不好的重要原因:当抛出错误时,运行时需要展开调用堆栈。就 CPU 周期而言,这比使用 if 或 switch 语句的常规分支要昂贵得多。
知道了这一点,你能证明仅仅因为滥用 try..catch 块而造成的性能损失是合理的吗?我说不。我想不出有什么理由能让前端世界对此感到非常满意。
既然我已经解释了我的推理,那么让我们回到正题吧。
HTTP 响应可能会根据其状态代码携带不同的信息。例如,接收 PATCH HTTP 请求的 todo 端点(例如 api/todos/:id)在响应状态代码为 200 时可能会返回具有不同正文的响应,而响应状态代码为 400 时可能会返回不同正文的响应。
举个例子:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
因此,考虑到这一点,我们回到问题陈述:如何键入一个执行此 PATCH 请求的函数,其中 TypeScript 可以告诉我正在处理哪个主体,具体取决于我编写的 HTTP 状态代码代码?答案:使用流式语法(构建器语法、链式语法)来累积类型。
让我们首先定义一个基于先前类型构建的类型:
export type AccumType<T, NewT> = T | NewT;
超级简单:给定类型 T 和 NewT,将它们连接起来形成一个新类型。在 AccumType 中再次使用这个新类型作为 T,然后就可以累积另一个新类型。然而,这手工完成的并不好。让我们介绍一下解决方案的另一个关键部分:流畅的语法。
给定类 X 的一个对象,其方法始终返回其自身(或自身的副本),可以将方法调用一个接一个地链接起来。这是流畅的语法,或者链式语法。
让我们编写一个简单的类来执行此操作:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
尤里卡!我们已经成功地将流畅的语法和我们编写的类型结合起来,开始将数据类型累积为单一类型!
如果不明显,您可以继续练习,直到积累了所需的类型(x.accumulate().accumulate()…直到完成)。
这一切都很好,但是这个超级简单的类型并没有将 HTTP 状态代码绑定到相应的正文类型。
我们想要的是为 TypeScript 提供足够的信息,以便其类型缩小功能发挥作用。为此,我们需要执行必要的操作来获取与原始问题相关的代码(在每个文件中键入 HTTP 响应的正文) -状态代码基础)。
首先,重命名并进化 AccumType。下面的代码显示了迭代的进展:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
此时,我意识到:状态代码是有限的:我可以(并且确实)查找它们并为它们定义类型,并使用这些类型来限制类型参数 TStatus:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
我们已经得到了一系列非常漂亮的类型:通过基于 ok 或 status 属性上的条件进行分支(编写 if 语句),TypeScript 的类型缩小功能将启动!不信,我们写一下class部分来试试:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
试驾一下:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
现在应该清楚为什么类型缩小能够根据 status 属性的 ok 属性正确预测分支时主体的形状。
但是,有一个问题:实例化类时的初始类型,在上面的注释块中标记。我是这样解决的:
export type AccumType<T, NewT> = T | NewT;
这个小改变有效地排除了最初的输入,我们现在开始营业了!
现在我们可以编写如下代码,Intellisense 将 100% 准确:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
在查询 ok 属性时,类型缩小也将起作用。
如果您没有注意到,我们可以通过不抛出错误来编写更好的代码。根据我的专业经验,axios 是错误的,ky 是错误的,任何其他 fetch helper 做同样的事情都是错误的。
TypeScript 确实很有趣。通过结合 TypeScript 和 Fluent 语法,我们能够根据需要积累尽可能多的类型,这样我们就可以从第一天开始编写更准确、更清晰的代码,而不是一遍又一遍地调试。这项技术已被证明是成功的,并且可供任何人尝试。安装 dr-fetch 并测试它:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
我还创建了 wj-config,一个旨在完全消除过时的 .env 文件和 dotenv 的软件包。该包还使用此处教授的 TypeScript 技巧,但它使用 & 而不是 | 连接类型。如果您想尝试一下,请安装 v3.0.0-beta.1。不过,打字要复杂得多。在 wj-config 之后进行 dr-fetch 简直就是在公园里散步。
让我们看看与 fetch 相关的包中的一些错误。
您可以在自述文件中看到:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
“服务器响应错误”??没有。 “服务器说你的请求不好”。是的,投掷部分本身就很糟糕。
这个想法是正确的,但不幸的是只能输入 OK 与非 OK 响应(最多 2 种类型)。
我最批评的软件包之一,展示了这个例子:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
这是一个非常初级的开发人员会写的:只是快乐的道路。根据其自述文件,等效性:
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
投掷部分太糟糕了:为什么要分支到投掷,强迫你稍后接住?这对我来说毫无意义。错误中的文本也具有误导性:它不是“获取错误”。抓取成功了。你得到了回应,不是吗?你只是不喜欢它……因为这不是快乐的道路。更好的措辞是“HTTP 请求失败:”。失败的是请求本身,而不是获取操作。
以上是如何使用 TypeScript 累积类型:输入所有可能的 fetch() 结果的详细内容。更多信息请关注PHP中文网其他相关文章!