首页 >web前端 >js教程 >如何使用 TypeScript 累积类型:输入所有可能的 fetch() 结果

如何使用 TypeScript 累积类型:输入所有可能的 fetch() 结果

Barbara Streisand
Barbara Streisand原创
2024-12-06 12:40:15880浏览

How to Use TypeScript to Accumulate Types: Typing ALL possible fetch() Results

当我开始(和我的团队)用 TypeScript 和 Svelte(我们都讨厌的 JavaScript 和 React)重写我们的应用程序时,我遇到了一个问题:

如何安全地输入 HTTP 响应的所有可能的正文?

这对你来说有启发吗?如果没有,你很可能就是“其中之一”,呵呵。让我们暂时离题一下,以便更好地理解图片。

为什么这个领域似乎未经探索

似乎没有人关心 HTTP 响应的“所有可能的主体”,因为我找不到为此做的任何东西(好吧,也许是 ts-fetch)。让我快速通过我的逻辑来解释为什么会这样。

没有人关心,因为人们:

  1. 只关心happy path:HTTP状态码为2xx时的响应体。

  2. 人们在其他地方手动输入它。

对于#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>;

“服务器响应错误”??没有。 “服务器说你的请求不好”。是的,投掷部分本身就很糟糕。

ts 获取

这个想法是正确的,但不幸的是只能输入 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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn