QCon 广州站即将开幕!60+ 场技术大咖演讲! 了解详情
写点什么

如何编写健壮的 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:211049

评论

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

华为云OBS助力企业解决数据存储难题

科技说

瑞萨E1/E20烧录工具自检方法

不脱发的程序猿

嵌入式 汽车电子 MCU 瑞萨 RH850

Atlassian FaaS 云开发平台 Forge 解析

跟YY哥学Jira

Jira Atlassian 云版 app development Forge

华为云数据库GaussDB(for Redis),如何为人们日常生活保驾护航

科技怪授

数据库

扩展接口SmartInstantiationAwareBeanPostProcessor解析

石臻臻的杂货铺

接口

dcm4che 依赖下载异常

JefferLiu

架构实战营 模块四作业

白杨

企业如何轻松上云?华为云弹性云服务器ECS给出答案

IT科技苏辞

华为云ECS,专为您打造安全、灵活、高效的应用环境

与时俱进的时代

Python数据可视化:数据分布图表可视化

不脱发的程序猿

Python 数据可视化 大数据分析

2023-01-12:一个n*n的二维数组中,只有0和1两种值, 当你决定在某个位置操作一次, 那么该位置的行和列整体都会变成1,不管之前是什么状态。 返回让所有值全变成1,最少的操作次数。 1 <

福大大架构师每日一题

算法 rust Solidity 福大大

华为云云原生数据库,激发数据活力

与时俱进的时代

华为云数据库,安全、专业,值得信赖

与时俱进的时代

本地数据怎么备份上云?华为云对象存储服务OBS帮你实现

科技说

华为云CDN为企业下载加速,极大提升客户体验

i生活i科技

CDN

华为云桌面Workspace荣获CSDN年度创新产品与解决方案大奖

Geek_2d6073

华为云大数据解决方案赋能金融行业发展,打造5G智慧银行营业厅

IT科技苏辞

华为云数字化解决方案激活企业市场空间,为企业稳健发展再加码!

IT科技苏辞

嗨 Jina,帮我画一幅高山流水图

Jina AI

图像生成 Diffusion

告别“自建房”,华为云ECS为企业提供更优选

科技说

华为云CDN全站加速助力网站性能全面优化

i生活i科技

CDN

CSDN 2022年度榜单揭晓,华为端云协同智能流程机器人斩获大奖

Geek_2d6073

华为云大数据赋能北港集团发展创新,数字化转型成效显著!

IT科技苏辞

【深入浅出Seata原理及实战】「入门基础专题」带你透析认识Seata分布式事务服务的原理和流程(1)

洛神灬殇

分布式事务 seata Alibaba SpringCloud Alibaba Seata框架

华为云OBS对象存储,企业存储的好帮手

科技说

安全、高效、便捷,华为云CDN助力企业体验升级!

i生活i科技

CDN

华为云CDN引领网站性能优化,助力企业更好发展

科技怪授

CDN

企业优秀网络环境,为何离不开华为云CDN全站加速服务?

科技怪授

CDN

Jira + GitLab 实践 DevOps

跟YY哥学Jira

DevOps gitlab jenkins Jira

看不见的控制流 — Rust 异步取消问题的几点思考

Greptime

数据库 rust 异步编程 tokio

如何编写健壮的TypeScript库?_运维_Chris Krycho_InfoQ精选文章