TypeScript 发布 4.9 beta
TypeScript 已于 2022.09.23 发布 4.9 beta 版本,你可以在 4.9 Iteration Plan 查看所有被包含的 Issue 与 PR。如果想要抢先体验新特性,执行:
$ npm install typescript@beta
来安装 beta 版本的 TypeScript,或在 VS Code 中安装 JavaScript and TypeScript Nightly 来更新内置的 TypeScript 支持。
本篇是笔者的第五篇 TypeScript 更新日志,接下来笔者还将持续更新 TypeScript 的 DevBlog 相关,感谢你的阅读。
另外,由于 beta 版本与正式版本通常不会有明显的差异,这一系列通常只会介绍 beta 版本而非正式版本。
鸽置的 ECMAScript 装饰器
--experimentalDecorators
与 --emitDecoratorMetadata
这两个配置仍然会被保留,用于启用旧版装饰器,而新版装饰器无需配置即会默认支持。非常合理,这么重磅的特性当然要版本凑个整才有仪式感。
如果你有兴趣了解更多装饰器的历史,可以阅读笔者的 走近MidwayJS:初识TS装饰器与IoC机制 中的介绍,或者贺师俊(Hax)老师在 是否应该在production里使用typescript的decorator?的回答。
satisfies 操作符
// 一块神奇调色板
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
interface Palette {
red: number[];
green: string;
blue: number[];
}
palette.green.startsWith('#'); // √ boolean
palette.red.startsWith('#'); // × 类型“number[]”上不存在属性“startsWith”。
const palette = {
red: [255, 0, 0],
// typo
grren: '#00ff00',
blue: [0, 0, 255],
};
type Colors = 'red' | 'green' | 'blue';
type RGB = [number, number, number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
// × 不存在此属性
grren: '#00ff00',
blue: [0, 0, 255],
};
看起来我们通过工具类型 + 字面量联合类型 + 元组类型完成了非常精确的类型标注,但现在又出现了新的问题,我们的调用出现了类型错误:
palette.green.startsWith('#'); // × 类型“string | RGB”上不存在属性“startsWith”
palette.red.startsWith('#'); // × 类型“string | RGB”上不存在属性“startsWith”
string | RGB
,而是仍然使用每个属性访问推导出的对应类型。const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
} satisfies Record<Colors, string | RGB>;
// string
palette.green.startsWith('#'); // √
// [number, number, number]
palette.red.find(); // √
// [number, number, number];
palette.blue.entries(); // √
你可能会觉得 satisfies 这个名字不太好理解,其实当时还有另一个候选的名字 implements,但由于它实际上已经是一个 JavaScript(ECMAScript)关键字,同时也已经被使用在 TypeScript Class 对抽象类以及接口的实现声明中,为了避免造成不必要的困惑,最后还是 satisfies 成功胜出。
type A = Record<Colors, string | RGB>;
type B = {
red: RGB;
green: string;
blue: RGB;
};
declare let a: A;
declare let b: B;
a = b; // √,说明子类型关系成立
b = a; // ×
satisfies Record<Colors, string | RGB>
时,实际上是在进行类型的向上转换,即 upcast。as Record<Colors, string | RGB>
。而如果你试了,就会明白为啥我不提它了。const palette = {
red: [255, 0, 0],
green: '#00ff00',
} as Record<Colors, string | RGB>;
palette.blue.includes();
class Animal {
eat() { }
}
class Dog extends Animal {
bark() { }
}
const dog = new Dog();
dog.eat();
class Cat extends Animal {
meow() { }
}
const wtfIsThisAnimal = new Animal()
if (wtfIsThisAnimal instanceof Dog) {
wtfIsThisAnimal.bark();
}
if (wtfIsThisAnimal instanceof Cat) {
wtfIsThisAnimal.meow();
}
ClassCastException
):Animal animal1 = new Animal();
Animal animal2 = new Dog();
Dog notADog = (Dog) animal1;
Dog actuallyADog = (Dog) animal2;
包含 .ts 后缀的导入路径支持
此特性并没有包含在 4.9 beta 中,而是被作为一个 4.9 整体的工作项,这里属于提前介绍。
'node'
会保持与 NodeJs 相同的解析策略,如对相对路径 ./foo
的解析,会首先尝试解析 <root>/<project>/src/foo.ts
与 <root>/<project>/src/foo.d.ts
,也即是说导入路径无需携带后缀名 .ts
。--noImplicitSuffix
选项(具体的选项配置还未最终确定)来修改了这一行为,在启用此选项时,导入路径必须显式携带 .ts
后缀才能正常解析,而不是依赖 moduleResolution 。import { writableStreamFromWriter } from "https://deno.land/std@0.156.0/streams/mod.ts";
单文件级别配置
此特性并没有包含在 4.9 beta 中,而是被作为一个 4.9 整体的工作项,这里属于提前介绍。
@ts-nocheck
与 @ts-check
指令来更改单个文件内的检查策略,如在关闭 checkJs
的情况下使用 @ts-check
来启用对少数几个 JS 文件的检查,或者在开启的情况下使用 @ts-nocheck
禁用对某些 JS 文件的检查。但这两个指令仅仅能影响是否检查,而无法影响检查的具体配置。@ts-config value
的形式,如以下示例:// @ts-strict
// @ts-noUnusedLocals
// @ts-strictNullChecks
// @ts-noPropertyAccessFromIndexSignature false
你可以查看 #49886 来了解所有已被支持的单文件配置。类似于 @ts-check
,这些指令必须被放在文件的顶部才能生效。
对未列出属性的类型收窄增强
in
关键字:interface LoginUser {
userId: string;
invitor: string;
}
interface Visitor {
visitorId: string;
from: string;
}
function checkUser(user: LoginUser | Visitor): string {
if ('userId' in user) {
return user.invitor
} else {
return user.from;
}
}
interface LoginUser {
userId: string;
invitor: string;
}
interface Visitor {
visitorId: string;
from: string;
}
function checkUser(info: { user: unknown }): string {
const user = info.user;
if (user && typeof user === "object") {
// 类型 object 上不存在属性 userId
if ('userId' in user && typeof user.userId === 'string') {
// 类型 object 上不存在属性 invitor
return user.invitor;
}
}
}
userId in user
这个条件,为什么在 typeof user.userId === 'string'
还会有属性不存在的报错?object & Record<"userId", unknown>
类型,这样就能够支持未列出属性的类型守卫了。另外,4.9 版本现在也会约束 in 操作符的左侧必须是 string / number / symbol 类型,以及右侧必须是 object 类型。
对 NaN 类型的相等检查
在 JavaScript 中 NaN 是一个特殊的数值类型值,ECMA262 标准中明确规定了它是一个基于 IEEE 754-2019 规范(IEEE Standard for Floating-Point Arithmetic. Institute of Electrical and Electronic Engineers, New York (2019))的“非正常数字”值。也就是说,它还是数值类型的值,但不是正常数字,或者说非数字。是不是有点混乱?在 ECMAScript 标准中规定了 Number 类型为上面提到的 IEEE 754 中 64 位浮点数实现,而 NaN 也就是遵循此标准的一个数字,并同样有重要的作用,如作为 0/0
这类运算的结果。
另外,null 类型是一个 Null 类型的值(也是唯一一个),虽然 typeof null 的结果是 object ,但这更多是历史包袱的原因。
而 NaN 实际上与任何类型的任何值都不等价(包括 NaN 本身),因此在判断一个值是不是 NaN 时,我们需要使用 Number.isNaN
而不是 value === NaN
。
TypeScript 4.9 版本新增了错误使用等价判断方式的提示:
// 此表达式将始终返回 false,你是否指 Number.isNaN(value) ?
if(value === NaN) {}
const obj = {};
// 此语句始终将返回 false,因为 JavaScript 中使用引用地址比较对象,而非实际值
if(obj === {}){}
其他更新
▐ Promise.resolve 的类型更新
type Awaited<T> = T extends null | undefined
? T
: T extends object & {
then(onfulfilled: infer F): any;
}
? F extends (value: infer V, ...args: any) => any
? Awaited<V>
: never
: T;
Promise.all
等方法,而在 4.9 版本中,则对 Promise.resolve
的类型签名也进行了替换:interface PromiseConstructor {
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
}
// 更新为
interface PromiseConstructor {
resolve<T>(value: T): Promise<Awaited<T>>;
resolve<T>(value: T | PromiseLike<T>): Promise<Awaited<T>>;
}
Promise.resolve
将尽可能返回一个 resolve 后的 Promise 类型值。▐ 完全保留 JavaScript 文件中的导入
import { someValue, SomeClass } from "some-module";
/** @type {SomeClass} */
let val = someValue;
这里 SomeClass 仅被作为 JSDoc 的类型标注使用,因此是可以直接从导入语句中被移除的。但这么做可能导致的问题是如果 JSDoc 类型标注不完全准确,就会导致这一擦除行为也表现异常。
因此,现在 JavaScript 文件中的导入语句将会被完全保留。
全文完,我们 5.0 beta 版本见:-)。
团队介绍
淘宝店铺作为电商基础链路,拥有亿级流量,同时也面临着无数机遇与挑战。店铺前端团队承载了服务于消费者的淘宝店铺页面、服务于商家的旺铺管理后台、服务于外部 ISV 开发商的模块体系以及其他相关业务,是消费者和商家之间最近的纽带。在过去、现在、未来,我们都将致力于提升商家的运营体验与消费者的购买体验。
前端 社群
下方加 Nealyang 好友回复「 加群」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章 点赞和在看就是最大的支持