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

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

Dec 06, 2024 pm 12:40 PM

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

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

前端热敏纸小票打印遇到乱码问题怎么办? 前端热敏纸小票打印遇到乱码问题怎么办? Apr 04, 2025 pm 02:42 PM

前端热敏纸小票打印的常见问题与解决方案在前端开发中,小票打印是一个常见的需求。然而,很多开发者在实...

神秘的JavaScript:它的作用以及为什么重要 神秘的JavaScript:它的作用以及为什么重要 Apr 09, 2025 am 12:07 AM

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

谁得到更多的Python或JavaScript? 谁得到更多的Python或JavaScript? Apr 04, 2025 am 12:09 AM

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

如何实现视差滚动和元素动画效果,像资生堂官网那样?
或者:
怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? 如何实现视差滚动和元素动画效果,像资生堂官网那样? 或者: 怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? Apr 04, 2025 pm 05:36 PM

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript的演变:当前的趋势和未来前景 JavaScript的演变:当前的趋势和未来前景 Apr 10, 2025 am 09:33 AM

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

JavaScript难以学习吗? JavaScript难以学习吗? Apr 03, 2025 am 12:20 AM

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? 如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? Apr 04, 2025 pm 05:09 PM

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

Zustand异步操作:如何确保useStore获取的最新状态? Zustand异步操作:如何确保useStore获取的最新状态? Apr 04, 2025 pm 02:09 PM

zustand异步操作中的数据更新问题在使用zustand状态管理库时,经常会遇到异步操作导致数据更新不及时的问题。�...

See all articles