【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

如何编写健壮的 TypeScript 库?

  • 2020-11-29
  • 本文字数:3402 字

    阅读完需:约 11 分钟

如何编写健壮的TypeScript库?

当你用 TypeScript 编写库时,你通常不知道这个库最终将如何被使用。即使你警告潜在用户,你编写这个库只是针对 TypeScript 用户,你还是可能会在某个时刻拥有 JavaScript 用户——或者是因为他们不顾你的警告而使用这个库,或者是他们因为传递性依赖而使用这个库。这有一个非常重要的后果:你必须将这个库设计成任何语言的开发者都可以使用!


其主要部分是函数定义和函数体。如果你针对一个纯 TypeScript 读者编写,那么你只需定义函数类型并信任编译器处理其它事情。如果你针对一个纯 JavaScrpit 读者编写,那么你需要记录那些类型,但在函数中将实际的类型设为unknown 并检查调用方传递的内容。


例如,给定如下代码——


interface Person {  age: number;  name?: string;}

function describe(person: Person): string { let name = person.name ?? 'someone'; return `${name} is ${person.age} years old!`;}
复制代码


一个 JS 用户可能用任何东西来调用 describe 函数。正确写法:


describe({ name: "chris" })
复制代码


灾难性的错误 


describe("potato");
复制代码


当然还有我们最喜欢犯的 JS 错误:


describe(undefined);
复制代码


你的库的 JS 用户并不是故意这么做的。恰恰相反!在任何足够大的系统中,很容易将错误的参数传递给系统中的某个函数。这通常是一个很难避免的错误,比如在一个点上做了修改,许多其它地方需要更新,但漏掉了一个点。故意的 JS 开发者会把坏数据发送到你设计精美的 TS API 中。


如果你针对一个纯TypeScript读者编写,那么你只需定义函数类型并信任编译器处理其它事情


我故意不提 TypeScript 编译非常严格,从一个与 JavaScript 没有区别的级别到几乎任何人可能想到的严格级别。这意味着,即使是 TypeScript 调用者也应该像 JavaScript 调用者一样被对待:众所周知,他们到处乱扔any ,忽略了事实上可能是null 或 undefined 的地方。返回上面的示例代码:


interface Person {  age: number;  name?: string;}

function describe(person: Person): string { let name = person.name ?? 'someone'; return `${name} is ${person.age} years old!`;}
复制代码


在没有启用严格标识的情况下,TypeScript 用户可以如下调用describe


function cueTheSobbing(data: any) {  describe(data);}

cueTheSobbing({ breakfastOf: ["eggs", "waffles"] });
复制代码


或者这样:


describe(null);
复制代码


或者这样:


describe({ age: null })
复制代码


也就是说:JS 调用者大部分会出错的方式,TS 调用者在关闭严格性设置的情况下也会出错。(你可以在这个TypeScript试验区查看所有这些“作品”!)这意味着故意的 TypeScript 用户也会用坏数据调用你的库。而且由于他们依赖其它库,这很可能不是他们的错误,因为这种问题可能发生在依赖图中的任何地方。


因此,如果问题是我们不能信任数据,那么我们应该怎么做?一个选项是使函数的所有参数实际为unknown ,并用JSDoc指定它该如何。然而,那样会使我们失去大量 TS 提供的能力。当与函数交互时,我们即使在内部也不会得到补全或类型错误,更不用说我们的库的用户。但是正如我们刚刚看到的,我们也不能依赖类型定义来提供函数内部的安全性。不过,我们可以将这几种方法结合起来:指定类型定义,并将传入的数据视为实际上的unknown。这确实带来了运行时开销——我们稍后将围绕这个权衡进行详细讨论。现在,我们可以先看看如何检查类型。


首先,我们会像实际上会从调用者得到真正未知的数据来编写我们的代码,因为我们已经确定了这正是我们可能得到的。一旦我们完成了对unknown 数据的校验,我们就能够将它替换为Person,而且所有东西都应该继续工作,但是现在我们可以保证它对任何抛给它的数据都能够工作。


function describe(person: unknown): string {  let name = person.name ?? 'someone';  return `${name} is ${person.age} years old`;}
复制代码


这里有类型错误(试验区),因为这里的person 类型可能是undefined"potato" 或者任何其它类型。我们可以使用 TypeScript 的类型缩小的概念来保证安全。然而,从unknown 缩小到特定的对象类型有点儿奇怪,因为如果你简单地检查是否typeof somethingUnknown === 'object' ,这会将类型缩小到{} ,这意味着它不会包含任何我们可能需要的类型。我们会先定义一个isObject 辅助函数,它会为我们提供正确的语义:


function isObject(  maybeObj: unknown): maybeObj is Record<string | number | symbol, unknown> {  return typeof maybeObj === 'object' && maybeObj !== null;}
复制代码

 

我们还需要一种方法来检查这个对象有没有指定的属性。如果in 运算符能以这种方式工作就太好了,但不幸的是,它没有这样工作。我们也可以内联这样做,但是每次都需要类型转换。我们可以称之为has ,类似于Object.hasOwnProperty 方法。由于这还需要检查isObject 返回的类型集——在 JS 中索引一个对象的所有合法类型——我们这里会将其提取到一个新的Key 类型。


这个has 辅助函数的返回类型告诉类型系统,如果主体为 true,传入的项目有其原始类型而且它包含我们要检查的属性。


type Key = string | number | symbol;

function has<K extends Key, T>( key: K, t: T): t is T & Record<K, unknown> { return key in t;}
复制代码


现在我们可以将它们组合成一个类型保护器,来检查给定对象是否是一个 person:


function isPerson(value: unknown): value is Person {  return (    isObject(value) &&    has('age', value) && typeof value.age === 'number' &&    (has('name', value) ? typeof value.name === 'string' : true)  )}
复制代码


现在,我们可以将所有这些集合到我们函数顶部的一个简单的检查中,如果它不合法的话抛出一个有用的错误。(你可以在这个试验区查看这个作品。)


function describe(person: unknown): string {  if (!isPerson(person)) {    throw new Error('`describe` requires you to pass a `Person`');  }

let name = person.name ?? 'someone'; return `${name} is ${person.age} years old`;}
复制代码


既然我们已经有了这个功能,我们可以将这里的person 类型更新为Person 来让 TypeScript 用户有更好的体验。


function describe(person: Person): string {  if (!isPerson(person)) {    throw new Error(      `'describe' takes a 'Person', but you passed ${JSON.stringify(person)}`    );  }

let name = person.name ?? 'someone'; return `${name} is ${person.age} years old`;}
复制代码


TypeScript 支持在条件不包含断言函数时抛出的这种模式泛化,这非常有用。我们可以编写如下格式:


function assert(  predicate: unknown,  message: string): asserts predicate {  if (!pred) {    throw new Error(message);  }}
复制代码


现在我们的函数变得更简单:


function describe(person: Person): string {  assert(    isPerson(person),    `'describe' takes a 'Person', but you passed ${JSON.stringify(person)}`  );

let name = person.name ?? 'someone'; return `${name} is ${person.age} years old`;}
复制代码


到目前为止,一直都还不错!我们现在保证,无论谁调用describe ,无论是从 JS,还是从松散类型的 TS,或是从其它完全不同的语言,它都会做“正确”的事情,在出错时向调用者提供一个可操作的错误。然而,根据我们的限制,这种运行时校验会开销过大而不可行。在一个浏览器中,我们通过网络发送的额外代码积累起来:需要下载更多东西,也需要解析更多东西,这都会减慢我们的 app。在任何环境中,每次与describe 函数交互时都会进行额外的运行时检查。


一种选项是利用一些编译智能来在开发期间而不是在生产构建中提供这些检查。Babel 允许你将给定函数转变成 noops,使得它们不完全没有开销,但开销非常小。例如,Ember CLI 提供了一个 Babel 插件将 Ember 的assert 函数(其类型与我在上面定义的assert 几乎等同)转变成 no-ops。你可以将它与任何可以消除无用代码的工具结合起来,以删除所有没有用到的辅助函数!


这种方案的缺点是,生产环境的错误的错误消息会比较糟糕,并且更难以调试。优点是,在生产环境中,你将上传更少的代码且运行时开销更少。为了使依赖这种assert 片段的代码工作,终端用户需要将它与任何具有良好的端到端测试覆盖的功能、UI 组件等相结合。但是不管怎样,这都是正确的:类型和测试消除了不同类型的 bugs,最好结合使用!


原文链接


Writing Robust TypeScript Libraries


2020-11-29 17:211177

评论

发布
暂无评论
发现更多内容

为开发者“铸剑”,一图揭秘华为云CodeArts IDE四大特性

华为云PaaS服务小智

云计算 开发者 华为云

Acrobat Pro DC 2023 for Mac(PDF编辑器) 中文最新

背包客

macos Mac软件 pdf编辑工具 Acrobat DC 2023 Acrobat DC

MegEngine 使用小技巧:如何做 MegCC 的模型性能评测

MegEngineBot

深度学习 开源 性能测试 编译器 MegEngine

HStore表全了解:实时入库与高效查询利器

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 6 月 PK 榜

vue使用Element-ui组件库-完整引入

我搬去水星了

6 月 优质更文活动

NFTScan 与 WalletFrens 达成合作伙伴,由 NFTScan 为其提供多链 NFT 数据服务!

NFT Research

合作 NFT\

建设数字工厂:生产物料齐套检查的实现方法

华为云开发者联盟

华为云 华为云开发者联盟 企业号 6 月 PK 榜

联盟送福利:云上掘金,开启你收入的第二增长曲线

华为云开发者联盟

云计算 华为云 华为云开发者联盟 企业号 6 月 PK 榜

GitHub 2800颗星,支持GPT/Transformer,字节跳动这个开源项目是怎么来的?

字节跳动技术范儿

机器学习 nlp

Downie 4 for Mac(最好用的视频下载软件)

背包客

macos Mac软件 mac软件下载 Downie 4 Mac版 Downie4

香港便宜虚拟主机:高性能、低成本,让你的网站事半功倍!

一只扑棱蛾子

香港主机 香港便宜虚拟主机

vue2.x与vue3.x中自定义指令详解

不叫猫先生

Vue 6 月 优质更文活动

2种GaussDB(DWS)查看作业运行信息方式

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 6 月 PK 榜

华为手环、耳机等礼品!等你拿~快来参与CodeArtsBuild实践体验!

华为云PaaS服务小智

云计算 华为 华为云

Vue路由守卫-前置路由守卫(beforeEach)

不觉心动

6 月 优质更文活动

dart-sass与node-sass的区别以及使用dart-sass可能会出现的问题

不叫猫先生

CSS SASS 6 月 优质更文活动

Vue-路由器的两种工作模式(history和hash)

不觉心动

6 月 优质更文活动

软件测试|抓包工具Charles--出现unknown怎么解决?

霍格沃兹测试开发学社

Spring Security6 全新写法,大变样!

江南一点雨

Java springboot springsecurity

数据驱动,支撑企业数智化平台升级

用友BIP

数智平台

ByteHouse+Apache Airflow:高效简化数据管理流程

字节跳动数据平台

airflow 数据管理 bytehouse

手势识别技术及其应用

来自四九城儿

生产环境可用的 Seata-go 1.2.0 来啦!!!

SOFAStack

Java seata 版本发布 Seata框架 Seata-go

Vue-后置路由守卫(afterEach)以及独享路由守卫(beforeEnter)

不觉心动

6 月 优质更文活动

喜报|瑞云科技荣获“年度汽车数字化营销供应商”奖

3DCAT实时渲染

云仿真 汽车三维可视化 汽车虚拟仿真

TS类型声明

不叫猫先生

typescript 6 月 优质更文活动

Linux常用命令

代码的路

前端基于DOM或者Canvas实现页面水印

不叫猫先生

Vue canvas 6 月 优质更文活动

风口上的AIGC,技术岗动不动年薪百万,甚至重金难求?

引迈信息

人工智能 低代码 AIGC ChatGPT

一文吃透低代码平台的衍生历程、优势及未来趋势

这我可不懂

低代码 数字化 JNPF

vue使用Element-ui组件库-按需引入

我搬去水星了

6 月 优质更文活动

如何编写健壮的TypeScript库?_软件工程_Chris Krycho_InfoQ精选文章