第2547期TypeScript 4.6 正式发布
Posted 前端早读课
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第2547期TypeScript 4.6 正式发布相关的知识,希望对你有一定的参考价值。
前言
周末愉快。今日前端早读课由@Hugo翻译授权分享。
正文从这开始~~
今天,我们发布了 TypeScript 4.6.
如果你还不熟悉 TypeScript,TypeScript 是在 javascript 之上构建的一个编程语言,并且为 JavaScript 提供了类型的语法。类型帮助你知道你的代码的变量和函数的种类。TypeScript 可以利用这些信息,帮助你消除拼写错误,或者是不小心忘记的 null 和 undefined 的检查。但是 TypeScript 提供的远比这些多,TypeScript 可以用这些信息极大的提升你的开发体验,提供例如代码补全,跳转定义,重命名等功能。如果你已经用 Visual Studio 或者 Visual Studio Code 进行编写 JavaScript 的项目,你其实已经间接使用了 TypeScript!
开始使用 TypeScript,你可以通过 NuGet,或者通过下面这个命令:
install typescript
如果你已经读了我们Beta 版本或者 RC 版本的博文,你可以直接看本次发布的变化部分。
下面是 TypeScript 4.6 新增的部分:
允许 class 的构造函数内 super() 前执行代码
对于解构 Discriminated 联合类型的控制流分析引擎(Control Flow Analysis)增强
增强递归深度检查
索引访问推断增强
Dependent 参数的控制流分析引擎增强
--target es2022
移除 react-jsx 不必要的参数
JSDoc 名字建议
对于 JavaScript 提供更多的语法和绑定错误提示
TypeScript Trace 分析工具
重大改变
对于 Beta 和 RC 版本以来的变化
当我们宣布 beta,我们没有提两个重要的功能:对于 Destructured Discriminated Unions 的控制流分析引擎增强和增加 --target es2022。从 beta 版本以来,还有一个值得注意的变化是:我们移除了react-jsx 的 void 0参数。
距离我们 RC 版本的变化主要是,对于不匹配 JSDoc 参数名的提示。
距离 RC 版本,我们还修复了一些 issues,修复了一些奇怪的报错信息,增强了某些场景大约 3% 的类型检查的速度。你可以在这里读到更详细的情况。
允许 class 的构造函数内 super() 前执行代码
在 JavaScript 中,你必须在调用 this 前强制执行 super()。TypeScript 也强制执行了这个约定,但是我们实现这个限制用了过于严格的限制。在之前的版本,如果在 super() 前调用任何代码都会报错:
Base
someProperty = constructor()
doSomeStuff();
这对于检查调用 this 前必须调用 super()变得很容易,但是这样做,你连不用 this 的代码,也不可以写了。TypeScript 4.6 现在允许你可以在 super() 前写不含有 this 的代码。
感谢 Joshua Goldberg 的 PR。
解构 Discriminated 联合类型的控制流分析引擎增强
TypeScript 可以收束名为 discriminant 属性的类型。例如,在下面的代码片段,TypeScript 可以通过 kind 的值收束 action 的类型。
=
|
| processAction( (=== // `类型.
let num = // ...
(=== // ` const str = // ...
这可以让我们用同一个 objects 存储不同类型的数据,但是需要手动添加一个字段,告诉 TypeScript,这个数据是什么。
这在使用 TypeScript 非常常见。但是,也许,你想更进一步,做下面这个例子,在条件判断前,提前对数据进行解构:
=
|
| processAction( const (=== let num = payload * // ...
(=== const str = payload. // ...
在之前的版本, TypeScript 会直接报错,一旦 kind 和 payload 进行解构,他们会认为是原有类型并集的独立的变量。
但是,在 TypeScript 4.6,这个可以工作了。
当使用 const 进行解构,或者解构以后,没有进行过重新赋值的情况下,TypeScript 可以记住从 discriminated 联合类型里解构的类型。在合适的情况下,解构出来的类型的关联依然保持,所以在上面的例子里,对于 kind 的收束可以获得对应的 payload 的类型。
对于更详细的信息,可以查看这个 PR。
增强递归深度检查
TypeScript 因为基于一个结构类型系统,并且还要提供范型,所以遇到很多有趣的挑战。
在一个结构类型系统中,object 类型可以通过他们有的成员的类似是否匹配来判断是否兼容。
Source
prop: string;
prop: check(source: Source, = source;
// 报错 // is not assignable to // Types of property are incompatible.
// is not assignable to Source<T>
prop: Source<Source<T>>;
prop: check(source: Source<string>, = source;
为了知道这个问题的答案,TypeScript 需要去检查 prop 的类型是否可以兼容。这带来另一个问题:Source<Source<string>>
和 Target<Target<number>>
的类型是否兼容呢?为了知道这个问题,TypeScript 又去检查 prop 的情况。就带来另一个递归检查,去检查 Source<Source<Source<string>>>
和 Target<Target<Target<number>>>
的情况。你会发现,这会一直迭代下去。
这里,TypeScript 需要一些启发式的方法,如果类型检查展开了足够的深度,TypeScript 就认为,这个类型有可能可以兼容。这一般来说,是可以的,但是遗憾的是,有下面的这样的例子:
prop: T;
x: Foo<Foo<Foo<Foo<Foo<Foo<y: Foo<Foo<Foo<Foo<Foo<
keyof TypeMap> = [in
kind: v: f: (p:
[keyof record.f(record.v);
kind: v: f: =>
console.log(
)
这个模式可以允许 TypeScript 知道 record.f(record.v) 是合法的,但是之前的版本,这个是有问题的。
TypeScript 4.6 之后,你不需要在调用 processRecord 前手动去做类型断言。
更多信息请参考这里。
Dependent 参数的控制流分析引擎增强
一个函数的参数签名可以通过展开参数的 discriminated tuples 联合类型来声明。
=> void;
const =>
(kind === payload.toFixed(); 收束到
(kind === payload.toUpperCase(); 收束到
;
f1(el =
foo
;
会被 TypeScript 编译为
jsx _jsx el = _jsx(, el = _jsx(, el = _jsx();
感谢 Alexander Tarasyuk 的 PR。
JSDoc 名字建议
在 JSDoc 里,你可以通过 @param 标签来标注参数的类型。
* @param x The first operand
* @param y The second operand
*/
x + y;
但是,如果这些注释过期了呢?比如我们吧 x 和 y 改名为 a 和b。
/**
* @param number * @param number */
function add(a, b)
return a + b;
之前,TypeScript 只会在 JavaScript 文件进行类型检查,当打开 checkJs 属性时,或者在一个文件最上面添加 // @ts-check 注释。
你现在可以在 TypeScript 文件里也获得相应的信息。TypeScript 现在会对不匹配的 JSDoc 注释进行一些提示。
Alexander Tarasyuk 提供了这个变化。
对于 JavaScript 提供更多的语法和绑定错误提示
TypeScript 对了 JavaScript 文件增加了很多语法和报错的提示。你现在通过 Visual Studio 或者 Visual Studio Code 可以看到这些新的报错,也可以通过 TypeScript 编译器跑 JavaScript 代码来看到这些信息(你都不需要增加 checkJs 或者 // @ts-check)。
例如,如果你声明两个同名 const 变量,TypeScript 就会给你报错。
foo = foo =
你可以通过增加 // @ts-nocheck 来关闭这些报错,但是我们也想知道,这些改动,对于 JavaScript 工作流有什么作用,有任何问题,欢迎来反馈。你可以在 Visual Studio Code 里安装 TypeScript 和 JavaScript 夜间扩展,和读这两个文章。
TypeScript Trace 分析工具
现在经常会有非常耗费性能的类型,让整个类型检查很慢。TypeScript 现在有 --generateTrace 标签来输出这些昂贵的类型,也可以通过这个报告来诊断一些 TypeScript 编译器的 issue。虽然这些信息有用,但是很难读。所以现在增加了可视化的观看方法。
我们最近发布了一个工具叫 @typescript/analyze-trace ,你可以通过这个工具来看一个图表的展示方式。我们不期望所有人都需要 analyze-trace,但是我们认为,这给一些性能问题提供了工具。
对于更多的信息,请看analyze-tracetool’s repo
重大改变
解构对象时丢弃范型对象的不可展开成员
现在对象展开时,对于不可展开的成员,会把对应的类型丢弃:
someProperty = someMethod()
someProperty, ...rest = x;
rest.someMethod();
rest 之前会有 Omit<T,"someProperty">
类型,因为TypeScript 会严格分析被解构的类型。因为在真正展开的运行情况和这个类型是不相符的。所以,在 4.6 中,rest 的类型,会变为 Omit<T,"someProperty"|"someMethod">
。
这个也会在 this 的解构中生效。当使用 ...rest 对 this 进行解构时,unspredable 和 non-public 成员,都会被丢弃。
someProperty = someMethod()
someOtherMethod()
someProperty, ...rest = rest.someMethod();
对于更多的信息,请看这里。
JavaScript 文件会一直受到语法和绑定的报错
之前,TypeScript 会忽略大部分 JavaScript 得语法报错,防止与 TypeScript 的语法混淆。现在 TypeScript 可以对 JavaScript 得语法进行校验,比如不正确的修饰符, 重复的声明,还有更多的东西。通过使用 Visual Studio Code 或者 Visual Studio 就可以获得这些能力,你也可以通过 TypeScript 编译器来实现。
你可以通过在 // @ts-nocheck 在文件顶部来关闭一个文件的检查。
可以看第一个和第二个这个功能的实现来详细了解这个功能。
下一步?
我们希望这次发布可以给你的代码之旅带来更多的快乐。如果你对于下一次发布也感兴趣,可以阅读我们对于 TypeScript 4.7 的规划。
Happy Hacking!
– Daniel Rosenwasser and the TypeScript Team
关于本文
译者:@Hugo
译文:https://zhuanlan.zhihu.com/p/473952171
作者:@Daniel
原文:https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/
关于Typescript还可以看看
【第2496期】现代Web应用应该是Bundless的
【第2522期】TypeScript 中的类型控制流分析演进
欢迎自荐投稿VX:zhgb_f2er,前端早读课等你来
第2217期typescript4.2新特性
前言
周末愉快。今日前端早读课由苏宁@chao wu授权分享。
正文从这开始~~
2021年2月23日,微软发布了typescript4.2版本,我们来看一下有哪些新的特性
更加智能的保留类型别名
TypeScript可以使用type定义一个类型,用来标识某个变量的类型,并且可以自动推断出赋值后新变量的类型,比如以下代码:
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
let x = value;
return x;
}
我们可以将上面的代码粘贴到TS Playground中运行,然后将鼠标hover到变量上,发现ts会自动推断出x变量的类型,如下图所示:
但是我们将代码稍做改造,如下:
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
if (Math.random() < 0.5) {
return undefined;
}
return value;
}
此时你猜想一下doStuff函数的返回值的类型,是BasicPrimitive | undefined ?
结果和你想的可能不一样,如下图所示:
那为什么会这样?
好吧,这与TypeScript如何在内部表示类型有关。当你从一个或多个联合类型创建新的联合类型时,它会将这些类型转成新的扁平化联合类型,但是这样做会丢失原有的类型信息。
在TypeScript 4.2中,内部结构就变得更加智能了,你可以在 TS Playground 中切换编译版本为4.2,你会发现类型推断很完美,如下图所示:
不可跟踪的rest元素
TS中我们可以用元组类型去标识一个数组的类型,例如:
let a: [string, number, boolean] = ['hello world', 10, false];
但是以上写法,元组中参数的个数是固定的,但如果number的数量是不固定的呢?
对TS熟悉的人可能会这么去写:
let a: [string, ...number[], boolean] = ['hello world', 10, false];
但这在4.2以下版本,会报以下错误:
原因就是number的数量是不可预知的,你必须将它放到boolean后面,但这会和我们的代码逻辑产生冲突。而这一切在4.2中会变得很和谐:
值得注意的是,如果你使用的是4.0版本,你可以这样修改你的代码,会发现报错也会消失(但需要注意的是,4.1依然会报错)
type Original = [string, ...number[]];
type NewType = [...Original, boolean, undefined];
let a: NewType = ['hello world', 10, false];
a = ['hello world', 10, 8, 3, false];
更进一步,我们可以用这个特性去定义一个“拥有任意个前导参数,且最后是几个固定类型的参数”,比如:
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
虽然rest元素可以处在元组的任意位置,但唯一的限制是“每个元组仅一个rest元素,在rest元素之后不能有其他rest元素”,举个例子:
interface Clown { /*...*/ }
interface Joker { /*...*/ }
let StealersWheel: [...Clown[], "me", ...Joker[]];
// ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.
let StringsAndMaybeBoolean: [...string[], boolean?];
// ~~~~~~~~ Error!
// An optional element cannot follow a rest element.
--noPropertyAccessFromIndexSignature
有如下代码:
interface Person {
/** 姓名 */
name: string;
/** 通过索引签名去扩充数据字段 */
[x: string]: any;
}
function processOptions(person: Person) {
console.log(`name: ${person.name}, age: ${person.age}`);
}
processOptions({ name: 'jack', age: 22 } as Person);
首先以上代码不会报错。
在代码中,age来自于索引签名,但往往为了区别于已知字段(比如name),用户可能会想让编译器报错,这时你可以在tsconfig.json中设置:
"noPropertyAccessFromIndexSignature": true,
然后重新启动前端项目,即可发现报错
Property 'age' comes from an index signature, so it must be accessed with ['age'].
抽象构造签名
有如下代码:
interface IShape {
getArea(): number;
}
class Shape implements IShape {
getArea() {
return 2;
}
}
function makeSubclassWithArea(Ctor: new () => IShape) {
return class extends Ctor {}
}
let MyShape = makeSubclassWithArea(Shape);
const a = new MyShape();
console.log(a.getArea()); // 2
上述代码功能很简单:
创建子类构造器,比如MyShape通过继承Shape构造器来创建。但是我们想通过抽象类来实现的话,代码可能会变成这样:
abstract class Shape {
// 在子类中去实现该方法
abstract getArea(): number;
}
interface IShape {
getArea(): number;
}
function makeSubclassWithArea(Ctor: new () => IShape) {
return class extends Ctor {
// 实现抽象类中的抽象函数
getArea() {
return 2;
}
}
}
let MyShape = makeSubclassWithArea(Shape);
但是遗憾的是,编译器会报错:
另外,如果使用InstanceType也会报同样的错:
这就是为什么TypeScript 4.2允许您在构造函数签名上指定抽象修饰符。以下是处理方案:
abstract class Shape {
// 在子类中去实现该方法
abstract getArea(): number;
}
type AbstractConstructor<T> = abstract new (...args: any[]) => T;
function makeSubclassWithArea<T extends AbstractConstructor<object>>(Ctor: T) {
abstract class SubClass extends Ctor {
// 实现抽象类中的抽象函数
getArea() {
return 2;
}
}
return SubClass;
}
class SubclassWithArea extends makeSubclassWithArea(Shape) {
customMethod() {
return this.getArea();
}
}
const a = new SubclassWithArea();
console.log(a.getArea()); // 2
console.log('customMethod result:' + a.customMethod()); // customMethod result: 2
使用--explainFiles了解您的项目结构
使用以下指令时,TypeScript编译器将给出一些非常长的输出,关于import信息。
tsc --explainFiles
# 如果全局没安装typescript,使用以下命令
# npx tsc --explainFiles
信息如下(提取自命令行工具):
...
node_modules/typescript/lib/lib.es5.d.ts
Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.es2015.d.ts'
Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.es2015.d.ts'
node_modules/@types/react/jsx-runtime.d.ts
Imported via "react/jsx-runtime" from file 'src/index.tsx' with packageId '@types/react/jsx-runtime.d.ts@17.0.2' to import 'jsx' and 'jsxs' factory functions
node_modules/@types/react/index.d.ts
Imported via 'react' from file 'src/framework/lib/BuryingPoint.tsx' with packageId '@types/react/index.d.ts@17.0.2'
Imported via 'react' from file 'node_modules/antd-mobile/lib/accordion/index.d.ts' with packageId '@types/react/index.d.ts@17.0.2'
Imported via 'react' from file 'node_modules/antd-mobile/lib/action-sheet/index.d.ts' with packageId '@types/react/index.d.ts@17.0.2'
node_modules/@types/react-router/index.d.ts
Imported via 'react-router' from file 'src/framework/routes/route.tsx' with packageId '@types/react-router/index.d.ts@5.1.11'
...
如果你觉得命令行工具中看的不舒服,可以将信息提取到txt或者vscode中
# 提前到txt
npx tsc --explainFiles > expanation.txt
# 提前到vscode
npx tsc --explainFiles | code -
改进逻辑表达式中的未调用函数检查
TypeScript的未调用函数检查现在适用于&&和||表达式。
在strictNullChecks: true下,以下代码现在将会报错。
function shouldDisplayElement(element: Element) {
// ...
return true;
}
function getVisibleItems(elements: Element[]) {
return elements.filter(e => shouldDisplayElement && e.children.length)
// ~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead.
}
解构变量可以明确标记为未使用
# 首先在tsconfig.json中配置noUnusedLocals为true
"noUnusedLocals": true,
以下代码中,_a未被使用(4.2以下版本会报以下错误)
const [_a, b] = [12, 3];
console.log(b); // TS6133: '_a' is declared but its value is never read.
你可能想要的是:告诉TS,以下划线开头的变量表示未使用变量,只负责占位,请不要报错。
此时,你只需要将ts版本升级为4.2即可(这确实是一个很重要的更新)。
但值得注意的是,以下代码,依然会报错:
const [a, b] = [12, 3];
console.log(b); // TS6133: 'a' is declared but its value is never read.
可选属性和字符串索引签名之间的宽松规则
先看一段代码(运行环境: < TS4.2),会报错:
type WesAndersonWatchCount = {
"Fantastic Mr. Fox"?: number;
"The Royal Tenenbaums"?: number;
"Moonrise Kingdom"?: number;
"The Grand Budapest Hotel"?: number;
};
// same as
// type WesAndersonWatchCount = {
// "Fantastic Mr. Fox"?: number | undefined;
// "The Royal Tenenbaums"?: number | undefined;
// "Moonrise Kingdom"?: number | undefined;
// "The Grand Budapest Hotel"?: number | undefined;
// }
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'. (2322)
然而上面的代码在4.2中是可以通过编译的,但是改造一下:
type WesAndersonWatchCount = { // 删除问号
"Fantastic Mr. Fox": number | undefined;
"The Royal Tenenbaums": number | undefined;
"Moonrise Kingdom": number | undefined;
"The Grand Budapest Hotel": number | undefined;
}
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.(2322)
// 以下场景在TypeScript 4.2也会报错
// Type 'undefined' is not assignable to type 'number'
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
。。。其他变化。。。
lib.d.ts 的更新
noImplicitAny错误适用于宽松的yeild表达式:
# 首先设置noImplicitAny为true
"noImplicitAny": true
然后在4.2中运行以下代码:
function* g1() {
const value = yield 1;
// ~~~~~~~
// Error!
// 'yield' expression implicitly results in an 'any' type
// because its containing generator lacks a return-type annotation.
}
function* g2() {
// No error.
// The result of `yield 1` is unused.
yield 1;
}
function* g3() {
// No error.
// `yield 1` is contextually typed by 'string'.
const value: string = yield 1;
}
function* g4(): Generator<number, void, string> {
// No error.
// TypeScript can figure out the type of `yield 1`
// from the explicit return type of `g3`.
const value = yield 1;
}
然而以上代码中g1方法在4.2以下版本不会报错。
in运算符不再允许在右侧使用基本类型
// [< 4.2] The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
"foo" in 42
// [= 4.2] The right-hand side of an 'in' expression must not be a primitive.
"foo" in 42
元组展开限制
TypeScript中可以使用扩展语法(...)来创建新的元组类型。
type NumStr = [number, string];
type NumStrNumStr = [...NumStr, ...NumStr];
但有时,这些元组类型可能会意外增长为巨大的类型,这可能会使类型检查花费很长时间。在4.2版本后,TypeScript设置了限制器以避免执行所有工作。
.d.ts扩展 不能在导入路径中使用
在TypeScript 4.2中,导入路径中包含.d.ts现在是错误的。
// must be changed something like
// - "./foo"
// - "./foo.js"
import { Foo } from "./foo.d.ts";
恢复模板字面量推断
declare const yourName: string;
const bar = `hello ${yourName}`;
type C = typeof bar;
下面分别展示的是4.1 和 4.2的不同:
但是如果你只想让此特性生效一次,你可以这样改造:
declare const yourName: string;
const bar = `hello ${yourName}` as const;
const baz = `hello ${yourName}`;
type C = typeof bar; // C -> `hello ${string}`
type D = typeof baz; // D -> string
值得一提的是4.1版本(4.1以下会报错),以下代码效果和4.2一致:
declare const yourName: string;
const bar = `hello ${yourName}` as const;
type C = typeof bar; // C -> `hello ${string}`
为你推荐
欢迎自荐投稿,前端早读课等你来
以上是关于第2547期TypeScript 4.6 正式发布的主要内容,如果未能解决你的问题,请参考以下文章
第1775期TypeScript:拥有超能力的 JavaScript (上)