说吐槽可能不太合适,所以通常使用运算符或类似例子中的技巧遍历检查每个层次的定义

  • 栏目:基础 时间:2020-05-01 19:24
<返回列表

近日名为 Evan Martin 的 Google 员工在 TypeScript 的 GitHub repo 中发表了对 TypeScript 的“吐槽”(就是提了一个 issue),说吐槽可能不太合适,准确来说是对 TypeScript 3.5 的使用反馈。

时间: 2019-12-07阅读: 13标签: 功能1. 可选链

时间: 2017-12-07阅读: 1361标签: 前端作者:陈达孚 ,来源:香港中文大学研究生,《移动Web前端高效开发实战》作者之一,《前端开发者指南2017》译者之一,在中国前端开发者大会,中生代技术大会等技术会议发表过主题演讲, 专注于新技术的调研和使用.

虽然 TypeScript 3.5 发布已有三个月(最新稳定版 3.6 已于上月月底发布),但 Google 开发团队最近才升级至 3.5 版本。使用一段时间后,开发者觉得不吐不快,于是便有了这篇质量颇高的使用反馈。是的,这里说的项目正是被众人使用的 Google —— 那个只有一个代码仓库且拥有数十亿行代码的 Google。

从 v3.7 可用

最近在做公司内部的一个的一个SDK的重构,这里总结一些经验分享给大家。

背景

开发团队面对的项目是拥有数十亿行代码的 Google,在团队内部,所有成员使用的是同一版本的 TypeScript 和同一组跨所有平台的编译器标记(compiler flag),如需升级,成员会协助为所有人同时升级这些标记。

Evan 说到,他和大家一样会期望 TypeScript 的新版本升级能带来一些改进。例如,Evan 表示自己希望并欢迎对标准库进行改进,即便这可能意味着需要从代码库中删除类似但不兼容的定义。但团队发现此次升级至 TypeScript 3.5 带来的额外工作量要比此前的升级多得多。

Evan 认为 3.5 版本中有三个主要变化让此次升级变得尤其艰难,他相信这些变化的大多数是有其目标的,并且旨在改进类型检查,但他也认为 TypeScript 团队所理解的类型检查始终只是在安全与效率之间权衡。

Evan 希望这份大型代码库的 TypeScript 使用反馈能帮助 TypeScript 团队更好地评估未来类似的情况,并提供一些建议。

下面看看 Evan 说的 3.5 版本给团队带来影响的三个主要变化。

这是当你尝试访问嵌套数据时的一个痛点,嵌套数据越多,代码就会变得越繁琐。

类型检查和智能提示

泛型的隐式默认值(Implicit default for generics)

此项特性属于 3.5 版本中的破坏性变化,Evan 认为这里导致出现问题的原因是代码的泛型与代码所做的工作并无相关。例如,假设有一些具有 Promise 解析的代码,但不关心 Promise 要解析的值:

function dontCarePromise() {
  return new Promise((resolve) => {
    resolve();
  });
}

由于泛型是未绑定的,在 3.4 中为 Promise<{}> 的代码在 3.5 中就会变为 Promise<unknown>。如果此函数的使用者在任意地方写下了这种类型的 Promise:

const myPromise: Promise<{}> = dontCarePromise();

这将会导致出现类型错误。

除此之外,还有一种被称为“仅返回泛型(return-only generics)”的模式,这种情况下,泛型函数仅在返回类型中使用它的任意模式。这里导致的问题是,会出现很多类型推导意外。例如,在只返回泛型的情况下,有如下的代码:

expectsString(myFunction());

可以按以下的方式合法重构:

const x = myFunction();
expectsString(x);

但最后发现,这是行不通的。

在下面的例子中,要访问address,你必须遍历data.customer.address,而且data或customer有可能是undefined,所以通常使用运算符或类似例子中的技巧遍历检查每个层次的定义。

作为一个SDK,我们的目标是让使用者能够减少查看文档的时间,所以我们需要提供一些类型的检查和智能提示,一般我们的做法是提供JsDoc,大部分编辑器可以提供快捷生成JsDoc的方式,我们比较常用的vscode可以使用Document This。

布尔过滤器 filter(Boolean)

TypeScript 3.5 更改了Boolean函数的类型,该函数会强制赋值给boolean,从

function Boolean(value?: any): boolean;

变为

function Boolean<T>(value?: T): boolean;

两者看起来可能非常相似。但试想一下,一个函数接受了一个谓词并返回一个数组过滤器,并像上面的代码一样使用:

function filter<T>(predicate: (t: T) => boolean): (ts: T[]) => T[];
const myFilter = filter(Boolean);

在 3.4 版本中,根据定义,T 从 any 变为myFilter,并成为一个由 any[]到 any[] 的函数。但在 3.5 版本中,T 只保留了泛型。

现在你可以用.?运算符来选择性地对数据访问。通过这种方式,如果存在尚未定义的父级对象,则会在链中的任何位置返回未定义,而不是在运行时崩溃。

另一种做法是使用Flow或者TypeScript,选择TypeScript的主要原因是自动生成的JsDoc比较原始,我们仍然需要在上面进行编辑,所以JsDoc维护和代码开发是脱离的,往往会出现代码更新了,JsDoc忘记更新的情况。

集合(Set)

在 TypeScript 3.4 中,下面的代码:

const s = new Set();

会返回一个 Set<any>。但 TypeScript 3.5 出现了一个变更,使得 lib.es2015.iterable.d.ts 具有移除 any 的效果,然后导致泛型改变上面的描述,并将类型推导为 unknown

这种变化最终很难修复,因为最终的类型错误有时与实际问题相差甚远。例如,在如下代码中:

class C {
  gather() {
    let s = new Set();
    s.add('hello');
    return s;
  }
  use(s: string[]) { … }
  demo() {
    this.use(Array.from(this.gather()));
  }
}

我们会收到关于 Array.from 类型错误的提示,但实际需要修复的是 new Set()

// v3.7 以前if (data  data.customer  data.customer.address) { const {address} = data.customer const fullAddress = `${address.street}, ${address.city}, ${address.state }${address.zipcode}`}// v3.7// data accessconst address = data?.customer?.addressconst fullAddress = `${address?.street}, ${address?.city}, ${address?.state } ${address?.zipcode}`// 也适用于数组customers?.[0]?.['address']// 检查方法是否已定义并调用customer.approve?.()

除此之外开发过程中我们无法享受到类型检查等对SDK开发比较重要的特性,TypeScript可以让我们减少犯错,减少调试的时间,另一方面这次开发的SDK在提供出去的时候就会进行一次相对简单的压缩,保证引入后的体积,所以会希望压缩掉JsDoc,而TypeScript可以通过在tsconfig.json中将declaration设置为true单独的d.ts文件。

最后

Evan 表示他们对 TypeScript 非常满意,此次的使用反馈只是希望能给团队在设计新特性时提供些许参考,以更好地开发 TypeScript。

(文/开源中国)    

  1. 空值合并

一个带提示的SDK:

从 v3.7 可用

最后,对于开发同学来说,就算不使用TypeScript,也强烈建议使用vscode提供//@ts-check注解,它会通过一些类型推导来检查你的代码的正确性,可以减少很多开发过程中的bug。

空值合并运算符是||的替代方法,如果左侧是null或undefined,则它返回右侧的表达式。这和||有什么不同?||本质上是 JavaScript 中的布尔 OR 运算符,我们尝试利用短路返回第一个非 false 值。这可能会产生意想不到的结果,因为当要求数字 0 或空字符串作为有效输入时,将会被视为false。让我们用一个例子来说明:

还有一个小技巧,如果你使用的库没有提供智能提示,你可以通过NPM/yarn的-D安装@types/{pkgname},这样你开发过程中就能够享受到vscode提供的智能提示,而-D安装到devDependencies中,也不会增加你在构建时的代码体积。

// 以前passPhrase = data.inputString || 'Unknown' //不会接受 "" (空字符串)passCode = data.number || '-1111' // 不会接受 0 rememberMe = data.rememberFlag || true // 将会总是 true!!!// 现在passPhrase = data.inputString ?? 'Unknown' //仅在 inputString 未定义时为 UnknownpassCode = data.number ?? '-1111' // 0 可以通过rememberMe = data.rememberFlag ?? true // false 是有效值

接口

通过这种方式可以明确地区分undefined与false的值。

既然提到了TypeScript,就提一下TypeScript的语法,基础类型没有必要赘述,而一些曾经的高级语法现在ES6也都能支持,这里提几点常用但是JavaScript开发者不太习惯使用的语法。

  1. 递归类型别名

很多人在开始使用TypeScript的时候,会很迷恋使用any或者默认的any,推荐在开发中打开tsconfig中的strict和noImplicitAny来保证尽量少的any使用,要知道,滥用any就等于你的类型检查并没有实质效果。

从 v3.7 可用

对一些暂时不能确定内容的对象的类型,可以使用{[key: string]: any},而不要直接使用any,后期可以慢慢扩展这个接口直到完全消除any,同时TypeScript的类型支持继承,在开发过程中,可以拆解接口,利用组合继承的方式减少重复定义。

现实世界中的很多数据类型都是递归的。例如,当你尝试处理分层数据时,会发现存在相同类型数据的重复模式。 JSON 是一个很好的例子,它本质上是一个哈希映射,而哈希映射本身可以包含另一个映射或映射数组。

但是接口也会带来一个小痛点,目前vscode的智能提醒不能很好的对应到接口,当你输入到对应变量的时候,虽然会高亮,但是高亮的也只是一个定义了名字的接口。没有办法直接看到接口里定义了什么。但是当你输入了接口里面定义的key的部分时,vscode会给你完整key的提示。虽然这对开发过程中有一点不够友好,但是vscode开发团队表示这是他们故意设计的,所以在API参数上可以选择将一些必要(重要)参数用基础类型直接使用,而将一些配置放入一个定义为接口的对象中。

在 v3.6 之前,如果必须定义一个简单的 JSON 类型,则必须像下面这样:

枚举

interface JSONObject { [x: string]: JSONValue; }interface JSONArray extends ArrayJSONValue { }type JSONValue = string | number | boolean | JSONObject | JSONArray

你有在代码中使用过:

如果你尝试将第 1 行和第 2 行的类型在像第 3 那样内联,则可能会出现以下错误:Type alias JSONValue circularly references itself。

const Platform = { ios: 0, android: 1}

在 v3.7 中已经有效解决了这个问题,可以像下面这样简单地进行编码:

那你在TypeScript中就应该使用枚举:

type JSONValue = string | number | boolean | { [x: string]: JSONValue } |ArrayJSONValue
enum Platform { ios, android}
  1. 断言签名

这样在函数中你就可以为某个参数设置类型为number,然后传入Platform.ios这样,枚举可以增加代码的维护性,它可以利用智能提示保证你输入的正确,不再会出现魔数(magic number)。相对于对象,它保证了输入的类型(你定义的对象可能某一天不再只有number类型的value),不再需要额外的类型判断。

从 v3.7 可用

装饰器

你应该知道 TypeScript 具有类型保护,可以很好地与 JavaScript 中的typeof和instanceOf运算符一起使用。这有助于为函数的参数添加前提条件,以便将其限制为特定的类型。

对于装饰器其实很多开发者既熟悉又陌生,在redux,mobx比较流行的现在,在代码中出现装饰器的调用已经很普遍,但是大多数开发者并没有将自己代码逻辑抽成装饰器的习惯。

让我们写一段把上面提到的这些东西都用到的代码,通过添加类型保护来确保给定的输入是日期,并从中提取年份:

比如在这个SDK的开发中,我们需要提供一些facade来兼容不同的平台(iOS, Android或者Web),而这个facade会通过插件的形式让开发者自己注册,SDK会维护一个注入后的对象,常规的使用方法是到了使用函数后判断环境再判断对象中有没有想有的插件,有就使用插件。

function isDate(input: unknown) : asserts input is Date { if (input instanceof Date) return; else throw new Error('Input must be a Date!'); } function getYear(input: unknown) : number { isDate(input); return input.getFullYear() // TypeScripts knows that input is Date } console.log(getYear(new Date('2019-01-01'))); console.log(getYear('2019-01-01')); 

实际来看,插件就是一个拦截器,我们只要阻止真正的函数运行就可以,大概的逻辑是这样的:

上面的代码看起来很不错,但 TypeScript 仍然会提示getFullYear 在未知类型上不可用。

export function facade(env: number) { return function( target: object, name: string, descriptor: TypedPropertyDescriptorany ) { let originalMethod = descriptor.value; let method; return { ...descriptor, value(...args: any[]): any { let [arg] = args; let { param, success, failure, polyfill } = arg; // 这部分可以自定义 if ((method = polyfill[env])) { method.use(param, success, failure); return; } originalMethod.apply(this, args); } }; };}

从 v3.7 开始,TypeScript 添加了一个名为asserts的新关键字,它能够使编译器从断言起就知道正确的类型。对于断言函数,应该添加asserts param as type而不是返回类型。

在SDK的开发过程中另一个常会遇到的就是很多参数的校验和再封装,我们也可以使用装饰器去完成:

这样,如果断言通过,TypeScript 将假定参数是前面定义的类型。修改后的代码如下所示:

export function snakeParam( target: object, name: string, descriptor: TypedPropertyDescriptorany) { let callback = descriptor.value!; return { ...descriptor, value(...args: any[]): any { let [arg, ...other] = args; arg = convertObjectName(arg, ConvertNameMode.toSnake); callback.apply(this, [arg, ...other]); } };}÷
function isDate(input: unknown) : asserts input is Date { if (input instanceof Date) return; else throw new Error('Input must be a Date!'); } function getYear(input: unknown) : number { isDate(input); return input.getFullYear() // TypeScripts knows that input is Date } console.log(getYear(new Date('2019-01-01'))); console.log(getYear('2019-01-01')); 

泛形

  1. 为 Promise 提供更好的反馈

泛形可以根据用户的输入决定输出,最简单的例子是

从 3.6 起改进

function identityT(arg: T): T { return arg;}

在代码中直接使用 Promise 而忘记使用await或then是常见的错误,如下所示:

当然它没有什么特别的意义,但是它表明了返回是根据arg的类型,在一般开发过程中,你逃不开范型的是Promise或者前面的TypedPropertyDescriptor这种内建的需要类型输入的地方,不要草率的使用any,如果你的后端返回是一个标准结构体类似:

interface Customer { name: string phone: string}declare function getCustomerData(id: string): PromiseCustomer;declare function payCustomer(customer: Customer): void;async function f() { const customer = getCustomerData('c1') payCustomer(customer)}
export interface IRes { status: number; message: string; data?: object;}

以前的 TypeScript 完全不了解 Promise,并显示一条与其无关的错误消息,如下所示:

那么你可以这样使用Promise:

从 v3. 6 开始,编译器变得非常聪明,可以建议你应该兑现 Promise。注意最新的编译器是如何处理相同的错误的:

function example(): PromiseIRes { return new Promise ...}

下面简单讨论一下不需要深入了解细节的一些功能:

当然泛形有很多高级应用,例如泛形约束,泛型创建工厂函数,已经超出了本文的范围,可以去官方文档了解。

  1. Unicode 标识符

构建

从 v3.6 可用

如果你的构建工具是Webpack,在SDK的开发中,尽量使用node方式调用(即webpack.run执行),因为SDK的构建往往会应对很多不同的参数变化,node方式相比纯配置方式可以更加灵活的调整输入输出的参数,也可以考虑使用rollup,rollup的构建代码更加面向编程方式。

consthello ="world"

需要注意的是,在Webpack3和rollup中构建中可以使用ES6模块化的方式构建,这样业务代码引入你的SDK后,可以通过解构引入的方式减少最终业务代码的体积,如果你只是提供了commonjs的包,那么构建工具的tree sharking是无法生效的,如果使用babel的话注意关闭module的编译。

上面的代码可能不能够在早期版本的 TypeScript 上编译,但是现在你可以从更广泛的 unicode 集中定义标识符。

另外一种减少单个包体积的方式,可以使用lerna在一个git仓库里构建多个NPM包,比起拆仓库可以更方便的使用公共部分的代码,但是也需要注意对公共部分代码的修改不要影响到别的包。

  1. 增量编译

其实对于大多数的SDK的来说,Webpack3和rollup使用感受是差不多的,比较常用的插件都有几乎同名的对应。不过rollup有两个优势,一个是rollup的构建更细化,rollup.rollup接受inputOptions生成bundle,还可以generate生成sourcemap,write生成output,在这个过程中我们可以做一些细致的工作。

从 v3.4 起可用

第二点是rollup.rollup会返回一个promise,也就意味着我们可以使用async的方式来写构建代码,而webpack.run还是使用的回调函数,虽然开发者可以封装成promise,但是个人觉得还是rollup的写法还是更爽一点。

如果你在大型项目上使用 TypeScript,则编译器可能需要很长时间才能响应你对该代项目中文件所做的更改。现在有了新的--incremental标志,你可以将其添加到tsc(typescript 编译器)命令行中,这个命令行将会递增地编译修改过的文件。

单元测试

TypeScript 通过把自从上次编译以来的项目信息保存在代码库内的本地缓存目录中来实现这一目的。在 React 代码库上,一定要记住在Webpack或Parcel进行正确的配置,这样才能在构建管道中利用增量编译。

上周我同事做了一个在线的分享,我发现很多同学都对单测很感兴趣也很疑惑,在前端开发中,对涉及UI的业务代码开发单测试比较困难的,但是对于SDK,单元测试肯定是准出的一个充要条件。当然其实我也很不喜欢写单测,因为单测往往比较枯燥,但是不写单测肯定会被老司机们“教育”的~_~。

原文:-new-and-exciting-typescript-features-48b760ae0b73

一般的单测使用mocha作为测试框架,expect作为断言库,使用nyc提供单测报告,一个大概的单测如下:

describe('xxx api test', function() { // 注意如果要用this调用mocha,不要用箭头函数 this.timeout(6000); it('xxx', done = { SDK.file .chooseImage({ count: 10, cancel: () = { console.log('选择图片取消----'); } }) .then(res = { console.dir(res); expect(res).to.be.an('object'); expect(res).to.have.keys('ids'); expect(res.ids).to.be.an('array'); expect(res.ids).to.have.length.above(0); uploadImg(res.ids); done(); }); });});

同样你可以用TypeScript写单测,当然在执行过程中,不需要再编译了,我们可以直接给mocha注册ts-node来直接执行,具体方式可以参考Write tests for TypeScript projects with mocha and chai — in TypeScript!。但是有一点需要提醒你,写单测的时候尽量依赖文档而不是智能提示,因为你的代码出错,可能会导致你的智能提示也是错误的,你根据错误的智能提示写的单测肯定也是。。。

对于网络请求的模拟可以使用nock这个库,需要在it之前增加一个beforeEach方法:

describe('proxy', () = { beforeEach(() = { nock('') .post('/test1') .delay(200) .reply(200, { // body test1: 1, test2: 2 }, { 'server-id': 'test' // header }); }); it(...}

最后我们用一个npm script加上nyc在mocha前面,就可以获得我们的单测报告了。

这里我还提了几个TypeScript使用中的小tips给大家参考。

tips: 如何在非发包情况下给内部库添加声明

这个SDK在开发过程会依赖一个内部NPM包,为了让这个NPM支持TypeScript调用,我们有几种做法:

给原包添加d.ts文件,然后发布.发布@types包,需要注意的是NPM不支持@types/@scope/{pkgname}这种写如果是私库包,可以使用@types/scope_{pkgname}这种写法.

这次使用的标注一个文件夹存放对应的d.ts文件,这种方式适合开发中进行,如果你觉得你写的d.ts还不够完美,或者这个d.ts文件目前只有这个SDK有需要,可以这么使用,在tsconfig.json中修改:

"baseUrl": "./","paths": { "*": ["/type/*"]}

tips: 如何处理resolve和reject不同类型的promise回调

默认的reject返回的参数类型是any,不一定能满足我们的需要,这里给一个解决方案,并非最佳,作为抛砖引玉:

{ then( onfulfilled?: | ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: | ((reason: U) => TResult2 | PromiseLike) | undefined | null ): IPromise; catch( onrejected?: | ((reason: U) => TResult | PromiseLike) | undefined | null ): Promise;" title="" data-original-title="复制">

interface IPromiseT, U { thenTResult1 = T, TResult2 = never( onfulfilled?: | ((value: T) = TResult1 | PromiseLikeTResult1) | undefined | null, onrejected?: | ((reason: U) = TResult2 | PromiseLikeTResult2) | undefined | null ): IPromiseTResult1 , TResult2; catchTResult = never( onrejected?: | ((reason: U) = TResult | PromiseLikeTResult) | undefined | null ): PromiseTResult;
上一篇:该公司正在为手机和计算机设备寻找替代的预装操作系统,华为MateBook预装运行Deepin发行版 下一篇:没有了

更多阅读

说吐槽可能不太合适,所以通常使用运算

基础 2020-05-01
近日名为 Evan Martin 的 Google 员工在 TypeScript 的 GitHub repo中发表了对 TypeScript的“吐槽”(就是...
查看全文

第一个 GNOME 3.36 开发快照,该项目发布了

基础 2020-04-30
GNOME 最新发布了代号为 “Thessaloniki” 的 3.34 版本,下一个稳定版 3.36也已被提上日程。 根据...
查看全文

该公司正在为手机和计算机设备寻找替代

基础 2020-04-30
自华为被美国列入实体清单以来,其产品的未来一直受到质疑。公司多次对外表示在硬件组件...
查看全文

友情链接: 网站地图

Copyright © 2015-2019 http://www.koi-bumi.com. 韦德体育有限公司 版权所有