三年回顾:JavaScript与TypeScript最新特性汇总

程序员成长指北

共 63861字,需浏览 128分钟

 ·

2023-06-20 10:32

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

作者 | Linus Schlumberger
译者 | 核子可乐
策划 | 丁晓昀
全文 7739 字,建议收藏。

本文涵盖了过去三年中发布的最新 JavaScript 和 TypeScript 特性,包括异步迭代器、nullish 合并、可选链、私有字段等等。对于每个特性,文章提供了简单的解释、示例代码以及使用场景,以便开发者能够更好地理解和应用这些新特性。同时,文章还介绍了如何在项目中使用这些特性,以及如何通过 polyfill 或者 Babel 等工具使旧的浏览器支持这些新特性。

本文将带大家回顾过去三年(乃至更早)以来,JavaScript/ECMAScript 以及 TypeScript 经历的一系列功能变化。

当然,这里提到的很多功能也许跟大家的日常工作八竿子打不着。但关注功能的发展变化,应该能帮助您加深对这些语言的理解。

有很多 TypeScript 功能并没有被记录在内,因为它们总体上可以概括为“之前它的运作效果跟期望不同,但现在相同了”。所以如果大家之前对某些问题有所诟病,现在不妨重试一次。

概述

●  JavaScript / ECMAScript (按时间排序)

●  TypeScript (按时间排序)

ECMAScript
更早(更早发布但仍然重要)

●  标记模板字面量:通过在模板字面量之前添加函数名,可以将函数传递至模板字面量和模板值的某些部分。这项功能有不少有趣的用途。

// Let's say we want to write a way to log arbitrary strings containing a number, but format the number.// We can use tagged templates for that.function formatNumbers(strings: TemplateStringsArray, number: number): string {  return strings[0] + number.toFixed(2) + strings[1];}console.log(formatNumbers`This is the value: ${0}, it's important.`); // This is the value: 0.00, it's important.// Or if we wanted to "translate" (change to lowercase here) translation keys within strings.function translateKey(key: string): string {  return key.toLocaleLowerCase();}function translate(strings: TemplateStringsArray, ...expressions: string[]): string {  return strings.reduce((accumulator, currentValue, index) => accumulator + currentValue + translateKey(expressions[index] ?? ''), '');}console.log(translate`Hello, this is ${'NAME'} to say ${'MESSAGE'}.`); // Hello, this is name to say message.

●  Symbols(之前被错误归类为 ES2022):对象的唯一键:Symbol("foo") === Symbol("foo"); // false。内部使用。

const obj: { [index: string]: string } = {};const symbolA = Symbol('a');const symbolB = Symbol.for('b');console.log(symbolA.description); // "a"obj[symbolA] = 'a';obj[symbolB] = 'b';obj['c'] = 'c';obj.d = 'd';console.log(obj[symbolA]); // "a"console.log(obj[symbolB]); // "b"// The key cannot be accessed with any other symbols or without a symbol.console.log(obj[Symbol('a')]); // undefinedconsole.log(obj['a']); // undefined// The keys are not enumerated when using for ... in.for (const i in obj) {  console.log(i); // "c", "d"}
ES2020

●  可选链:要访问一个可能未定义的对象的值(通过索引),可以通过在父对象名称后添加? 来使用可选链。可选链也可用于索引 ([...]) 或者函数调用。

// PREVIOUSLY:// If we have an object variable (or any other structure) we don't know for certain is defined,// We can not easily access the property.const object: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };const value = object.name; // type error: 'object' is possibly 'undefined'.// We could first check if it is defined, but this hurts readability and gets complex for nested objects.const objectOld: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };const valueOld = objectOld ? objectOld.name : undefined;// NEW:// Instead we can use optional chaining.const objectNew: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };const valueNew = objectNew?.name;// This can also be used for indexing and functions.const array: string[] | undefined = Math.random() > 0.5 ? undefined : ['test'];const item = array?.[0];const func: (() => string) | undefined = Math.random() > 0.5 ? undefined : () => 'test';const result = func?.();

●  import(): 动态导入,例如 import ... from ...,但在运行上且使用变量。

let importModule;if (shouldImport) {importModule = await import('./module.mjs');}

●  String.matchAll: 获取正则表达式的多个匹配项,包括其捕获组,且不使用循环。

const stringVar = 'testhello,testagain,';// PREVIOUSLY:// Only gets matches, but not their capture groups.console.log(stringVar.match(/test([\w]+?),/g)); // ["testhello,", "testagain,"]// Only gets one match, including its capture groups.const singleMatch = stringVar.match(/test([\w]+?),/);if (singleMatch) {  console.log(singleMatch[0]); // "testhello,"  console.log(singleMatch[1]); // "hello"}// Gets the same result, but is very unintuitive (the exec method saves the last index).// Needs to be defined outside the loop (to save the state) and be global (/g),// otherwise this will produce an infinite loop.const regex = /test([\w]+?),/g;let execMatch;while ((execMatch = regex.exec(stringVar)) !== null) {  console.log(execMatch[0]); // "testhello,", "testagain,"  console.log(execMatch[1]); // "hello", "again"}// NEW:// Regex needs to be global (/g), also doesn't make any sense otherwise.const matchesIterator = stringVar.matchAll(/test([\w]+?),/g);// Needs to be iterated or converted to an array (Array.from()), no direct indexing.for (const match of matchesIterator) {  console.log(match[0]); // "testhello,", "testagain,"  console.log(match[1]); // "hello", "again"}

●  Promise.allSettled(): 与 Promise.all() 类似,但需要等待所有 Promises 完成,且不会在第一次 reject/throw 时返回。它能让降低错误处理的难度。

async function success1() {return 'a'}async function success2() {return 'b'}async function fail1() {throw 'fail 1'}async function fail2() {throw 'fail 2'}// PREVIOUSLY:console.log(await Promise.all([success1(), success2()])); // ["a", "b"]// but:try {  await Promise.all([success1(), success2(), fail1(), fail2()]);} catch (e) {  console.log(e); // "fail 1"}// Notice: We only catch one error and can't access the success values.// PREVIOUS FIX (really suboptimal):console.log(await Promise.all([ // ["a", "b", undefined, undefined]  success1().catch(e => { console.log(e); }),  success2().catch(e => { console.log(e); }),  fail1().catch(e => { console.log(e); }), // "fail 1"  fail2().catch(e => { console.log(e); })])); // "fail 2"// NEW:const results = await Promise.allSettled([success1(), success2(), fail1(), fail2()]);const sucessfulResults = results  .filter(result => result.status === 'fulfilled')  .map(result => (result as PromiseFulfilledResult<string>).value);console.log(sucessfulResults); // ["a", "b"]results.filter(result => result.status === 'rejected').forEach(error => {  console.log((error as PromiseRejectedResult).reason); // "fail 1", "fail 2"});// OR:for (const result of results) {  if (result.status === 'fulfilled') {    console.log(result.value); // "a", "b"  } else if (result.status === 'rejected') {    console.log(result.reason); // "fail 1", "fail 2"  }}

●  globalThis: 在全局上下文中访问变量,与环境无关(浏览器、NodeJS 等)。仍被视为较差实践,但在某些情况下是必要的。类似于浏览器上的 this。

console.log(globalThis.Math); // Math Object

import.meta: 在使用 ES-modules 时,获取当前模块的 URL import.meta.url。

console.log(import.meta.url); // "file://..."

●  export * as … from …: 轻松将默认值重新导出为子模块。

export * as am from 'another-module'import { am } from 'module'
ES2021

●  String.replaceAll(): 替换掉某字符串内某一子字符串的所有实例,无需始终使用带有全局标志(/g)的正则表达式。

const testString = 'hello/greetings everyone/everybody';// PREVIOUSLY:// Only replaces the first instanceconsole.log(testString.replace('/', '|')); // 'hello|greetings everyone/everybody'// Instead a regex needed to be used, which is worse for performance and needs escaping.// Not the global flag (/g).console.log(testString.replace(/\//g, '|')); // 'hello|greetings everyone|everybody'// NEW:// Using replaceAll this is much clearer and faster.console.log(testString.replaceAll('/''|')); // 'hello|greetings everyone|everybody'

●  Promise.any: 当只需要获取 promises 列表中的一个结果时,则返回第一个结果;仅在所有 promises 均被拒绝时才返回 AggregateError,而非立即拒绝的 Promise.race。

async function success1() {return 'a'}async function success2() {return 'b'}async function fail1() {throw 'fail 1'}async function fail2() {throw 'fail 2'}// PREVIOUSLY:console.log(await Promise.race([success1(), success2()])); // "a"// but:try {  await Promise.race([fail1(), fail2(), success1(), success2()]);} catch (e) {  console.log(e); // "fail 1"}// Notice: We only catch one error and can't access the success value.// PREVIOUS FIX (really suboptimal):console.log(await Promise.race([ // "a"  fail1().catch(e => { console.log(e); }), // "fail 1"  fail2().catch(e => { console.log(e); }), // "fail 2"  success1().catch(e => { console.log(e); }),  success2().catch(e => { console.log(e); })]));// NEW:console.log(await Promise.any([fail1(), fail2(), success1(), success2()])); // "a"// And it only rejects when all promises reject and returns an AggregateError containing all the errors.try {  await Promise.any([fail1(), fail2()]);} catch (e) {  console.log(e); // [AggregateError: All promises were rejected]  console.log(e.errors); // ["fail 1", "fail 2"]}

●  Nullish coalescing assignment (??=): 仅在之前为 “nullish”(null 或 undefined)时才分配值。

let x1 = undefined;let x2 = 'a';const getNewValue = () => 'b';// Assigns the new value to x1, because undefined is nullish.x1 ??= 'b';console.log(x1) // "b"// Does not assign a new value to x2, because a string is not nullish.// Also note: getNewValue() is never executed.x2 ??= getNewValue();console.log(x1) // "a"

●  Logical and assignment (&&=): 仅在之前为“truhty”(true 或可以转换为 true 的值)时才分配值。

let x1 = undefined;let x2 = 'a';const getNewValue = () => 'b';// Does not assign a new value to x1, because undefined is not truthy.// Also note: getNewValue() is never executed.x1 &&= getNewValue();console.log(x1) // undefined// Assigns a new value to x2, because a string is truthy.x2 &&= 'b';console.log(x1) // "b"

●  Logical or assignment (||=): 仅在之前为“falsy”(false 或转换为 false)时才分配值。

let x1 = undefined;let x2 = 'a';const getNewValue = () => 'b';// Assigns the new value to x1, because undefined is falsy.x1 ||= 'b';console.log(x1) // "b"// Does not assign a new value to x2, because a string is not falsy.// Also note: getNewValue() is never executed.x2 ||= getNewValue();console.log(x1) // "a"

●  WeakRef: 保留对一个对象的“weak”引用,但不阻止对象被垃圾回收。

const ref = new WeakRef(element);// Get the value, if the object/element still exists and was not garbage-collected.const value = ref.deref;console.log(value); // undefined// Looks like the object does not exist anymore.

●  数字分隔符 (_): 使用 _ 分隔数字以提高可读性。不会对功能造成影响。

const int = 1_000_000_000;const float = 1_000_000_000.999_999_999;const max = 9_223_372_036_854_775_807n;const binary = 0b1011_0101_0101;const octal = 0o1234_5670;const hex = 0xD0_E0_F0;


ES2022

●  #private: 通过以 # 开头的命名,使类成员(属性和方法)私有,即只能通过类本身进行访问。其无法被删除或动态分配。任何不正确行为都会导致 JavaScript(注意,不是 TypeScript)语法错误。不推荐在 TypeScript 项目中这样做,而应用直接使用 private 关键字。

class ClassWithPrivateField {#privateField;#anotherPrivateField = 4;constructor() {this.#privateField = 42; // Validthis.#privateField; // Syntax errorthis.#undeclaredField = 444; // Syntax errorconsole.log(this.#anotherPrivateField); // 4}}const instance = new ClassWithPrivateField();instance.#privateField === 42; // Syntax error

●  静态类成员: 将任意类字段(属性和方法)标记为静态。

class Logger {static id = 'Logger1';static type = 'GenericLogger';static log(message: string | Error) {console.log(message);}}class ErrorLogger extends Logger {static type = 'ErrorLogger';static qualifiedType;static log(e: Error) {return super.log(e.toString());}}console.log(Logger.type); // "GenericLogger"Logger.log('Test'); // "Test"// The instantiation of static-only classes is useless and only done here for demonstration purposes.const log = new Logger();ErrorLogger.log(new Error('Test')); // Error: "Test" (not affected by instantiation of the parent)console.log(ErrorLogger.type); // "ErrorLogger"console.log(ErrorLogger.qualifiedType); // undefinedconsole.log(ErrorLogger.id); // "Logger1"// This throws because log() is not an instance method but a static method.console.log(log.log()); // log.log is not a function

●  类中的静态初始化块: 类初始化时运行的块,基本属于静态成员的“构造函数”。

class Test {static staticProperty1 = 'Property 1';static staticProperty2;static {this.staticProperty2 = 'Property 2';}}console.log(Test.staticProperty1); // "Property 1"console.log(Test.staticProperty2); // "Property 2"

●  导入断言(非标准,在 V8 中实现):以 import ... from ... assert { type: 'json' }的形式对导入类型做断言,可用于在不解析 JSON 的前提下将其导入。

import json from './foo.json' assert { type: 'json' };console.log(json.answer); // 42

●  RegExp 匹配索引:获取正则表达式匹配和捕获组的开始和结束索引。适用于 RegExp.exec(), String.match() 和 String.matchAll()。

const matchObj = /(test+)(hello+)/d.exec('start-testesthello-stop');// PREVIOUSLY:console.log(matchObj?.index);// NEW:if (matchObj) {// Start and end index of entire match (before we only had the start).console.log(matchObj.indices[0]); // [9, 18]// Start and end indexes of capture groups.console.log(matchObj.indices[1]); // [9, 13]console.log(matchObj.indices[2]); // [13, 18]}

●  负索引 (.at(-1)): 在索引数组或字符串时,可以使用 at 从末尾开始索引。相当于 arr[arr.length - 1)

console.log([4, 5].at(-1)) // 5

●  hasOwn: 推荐使用的新方法,用于查找对象具有哪些属性,用于替代 obj.hasOwnProperty()。在某些特殊情况下效果更好。

const obj = { name: 'test' };console.log(Object.hasOwn(obj, 'name')); // trueconsole.log(Object.hasOwn(obj, 'gender')); // false

●  错误原因(Error cause): 现在可以为错误指定可选原因,允许在重新抛出时指定原始错误。

try {try {connectToDatabase();} catch (err) {throw new Error('Connecting to database failed.', { cause: err });}} catch (err) {console.log(err.cause); // ReferenceError: connectToDatabase is not defined}
之后(已可在 TypeScript 4.9 中使用)

●  Auto-Accessor: 自动将属性设为私有,并为其创建 get/set 访问器。

class Person {accessor name: string;constructor(name: string) {this.name = name;console.log(this.name) // 'test'}}const person = new Person('test');
TypeScript
基础(进一步介绍上下文)

●  泛型: 将类型传递至其他类型,负责在对类型进行泛化后仍保证类型安全。应始终优先使用泛型,而非 any 或 unknown。

// WITHOUT:function getFirstUnsafe(list: any[]): any {return list[0];}const firstUnsafe = getFirstUnsafe(['test']); // typed as any// WITH:function getFirst<Type>(list: Type[]): Type {return list[0];}const first = getFirst<string>(['test']); // typed as string// In this case the parameter can even be dropped because it is inferred from the argument.const firstInferred = getFirst(['test']); // typed as string// The types accepted as generics can also be limited using `extends`. The Type is also usually shortened to T.class List<T extends string | number> {private list: T[] = [];get(key: number): T {return this.list[key];}push(value: T): void {this.list.push(value);}}const list = new List<string>();list.push(9); // Type error: Argument of type 'number' is not assignable to parameter of type 'string'.const booleanList = new List<boolean>(); // Type error: Type 'boolean' does not satisfy the constraint 'string | number'.
更早(更早发布但仍然重要)

●  实用程序类型: TypeScript 中包含多种实用程序类型,这里解释其中最重要的几种。

interface Test {  name: string;  age: number;}// The Partial utility type makes all properties optional.type TestPartial = Partial<Test>; // typed as { name?: string | undefined; age?: number | undefined; }// The Required utility type does the opposite.type TestRequired = Required<TestPartial>; // typed as { name: string; age: number; }// The Readonly utility type makes all properties readonly.type TestReadonly = Readonly<Test>; // typed as { readonly name: string; readonly age: string }// The Record utility type allows the simple definition of objects/maps/dictionaries. It is preferred to index signatures whenever possible.const config: Record<string, boolean> = { option: false, anotherOption: true };// The Pick utility type gets only the specified properties.type TestLess = Pick<Test, 'name'>; // typed as { name: string; }type TestBoth = Pick<Test, 'name' | 'age'>; // typed as { name: string; age: string; }// The Omit utility type ignores the specified properties.typetype TestFewer = Omit<Test, 'name'>; // typed as { age: string; }type TestNone = Omit<Test, 'name' | 'age'>; // typed as {}// The Parameters utility type gets the parameters of a function type.function doSmth(value: string, anotherValue: number): string {  return 'test';}type Params = Parameters<typeof doSmth>; // typed as [value: string, anotherValue: number]// The ReturnType utility type gets the return type of a function type.type Return = ReturnType<typeof doSmth>; // typed as string// There are many more, some of which are introduced further down.

●  条件类型: 根据某种类型是否匹配 / 扩展另一种类型,来对类型做有条件设置。可以按照 JavaScript 中条件(三元)运算符的方式理解。

// Only extracts the array type if it is an array, otherwise returns the same type.type Flatten<T> = T extends any[] ? T[number] : T;// Extracts out the element type.type Str = Flatten<string[]>; // typed as string// Leaves the type alone.type Num = Flatten<number>; // typed as number

●  使用条件类型进行推断: 并非所有泛型类型都需要由用户指定,有些也可以从代码中推断得出。要实现基于类型推断的条件逻辑,必须有 infer 关键字,它会以某种方式定义临时推断类型变量。

// Starting with the previous example, this can be written more cleanly.type FlattenOld<T> = T extends any[] ? T[number] : T;// Instead of indexing the array, we can just infer the Item type from the array.type Flatten<T> = T extends (infer Item)[] ? Item : T;// If we wanted to write a type that gets the return type of a function and otherwise is undefined, we could also infer that.type GetReturnType<Type> = Type extends (...args: any[]) => infer Return ? Return : undefined;type Num = GetReturnType<() => number>; // typed as numbertype Str = GetReturnType<(x: string) => string>; // typed as stringtype Bools = GetReturnType<(a: boolean, b: boolean) => void>; // typed as undefined

●  元组可选元素与其余元素: 使用 ? 声明元组中的可选元素,使用 ... 声明元组中的其余元素。

// If we don't yet know how long a tuple is going to be, but it's at least one, we can specify optional types using `?`.const list: [number, number?, boolean?] = [];list[0] // typed as numberlist[1] // typed as number | undefinedlist[2] // typed as boolean | undefinedlist[3] // Type error: Tuple type '[number, (number | undefined)?, (boolean | undefined)?]' of length '3' has no element at index '3'.// We could also base the tuple on an existing type.// If we want to pad an array at the start, we could do that using the rest operator `...`.function padStart<T extends any[]>(arr: T, pad: string): [string, ...T] {  return [pad, ...arr];}const padded = padStart([1, 2], 'test'); // typed as [string, number, number]

●  抽象类和方法: 类和类中的各方法可以被声明为 abstract,以防止其被实例化。

abstract class Animal {  abstract makeSound(): void;  move(): void {    console.log('roaming the earth...');  }}// Abstract methods need to be implemented when extended.class Cat extends Animal {} // Compile error: Non-abstract class 'Cat' does not implement inherited abstract member 'makeSound' from class 'Animal'.class Dog extends Animal {  makeSound() {    console.log('woof');  }}// Abstract classes cannot be instantiated (like Interfaces), and abstract methods cannot be called.new Animal(); // Compile error: Cannot create an instance of an abstract class.const dog = new Dog().makeSound(); // "woof"

●  构造函数签名: 在类声明之外,定义构造函数的类型。在大多数情况下不应使用,建议用抽象类代替。

interface MyInterface {  name: string;}interface ConstructsMyInterface {  new(name: string): MyInterface;}class Test implements MyInterface {  name: string;  constructor(name: string) {    this.name = name;  }}class AnotherTest {  age: number;}function makeObj(n: ConstructsMyInterface) {    return new n('hello!');}const obj = makeObj(Test); // typed as Testconst anotherObj = makeObj(AnotherTest); // Type error: Argument of type 'typeof AnotherTest' is not assignable to parameter of type 'ConstructsMyInterface'.

●  ConstructorParameters Utility 类型: 属于 TypeScript 辅助函数,能够根据构造函数类型(但不是类)获取构造函数参数。

// What if we wanted to get the constructor argument for our makeObj function.interface MyInterface {  name: string;}interface ConstructsMyInterface {  new(name: string): MyInterface;}class Test implements MyInterface {  name: string;  constructor(name: string) {    this.name = name;  }}function makeObj(test: ConstructsMyInterface, ...args: ConstructorParameters<ConstructsMyInterface>) {  return new test(...args);}makeObj(Test); // Type error: Expected 2 arguments, but got 1.const obj = makeObj(Test, 'test'); // typed as Test
TypeScript 4.0

●  可变元组类型: 元组中的其余元素现在是通用的,且允许使用多个其余元素。

// What if we had a function that combines two tuples of undefined length and types? How can we define the return type?// PREVIOUSLY:// We could write some overloads.declare function concat(arr1: [], arr2: []): [];declare function concat<A>(arr1: [A], arr2: []): [A];declare function concat<A, B>(arr1: [A], arr2: [B]): [A, B];declare function concat<A, B, C>(arr1: [A], arr2: [B, C]): [A, B, C];declare function concat<A, B, C, D>(arr1: [A], arr2: [B, C, D]): [A, B, C, D];declare function concat<A, B>(arr1: [A, B], arr2: []): [A, B];declare function concat<A, B, C>(arr1: [A, B], arr2: [C]): [A, B, C];declare function concat<A, B, C, D>(arr1: [A, B], arr2: [C, D]): [A, B, C, D];declare function concat<A, B, C, D, E>(arr1: [A, B], arr2: [C, D, E]): [A, B, C, D, E];declare function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];declare function concat<A, B, C, D>(arr1: [A, B, C], arr2: [D]): [A, B, C, D];declare function concat<A, B, C, D, E>(arr1: [A, B, C], arr2: [D, E]): [A, B, C, D, E];declare function concat<A, B, C, D, E, F>(arr1: [A, B, C], arr2: [D, E, F]): [A, B, C, D, E, F];// Even just for three items each, this is really suboptimal.// Instead we could combine the types.declare function concatBetter<T, U>(arr1: T[], arr2: U[]): (T | U)[];// But this types to (T | U)[]// NEW:// With variadic tuple types, we can define it easily and keep the information about the length.declare function concatNew<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U];const tuple = concatNew([23, 'hey', false] as [number, string, boolean], [5, 99, 20] as [number, number, number]);console.log(tuple[0]); // 23const element: number = tuple[1]; // Type error: Type 'string' is not assignable to type 'number'.console.log(tuple[6]); // Type error: Tuple type '[23, "hey", false, 5, 99, 20]' of length '6' has no element at index '6'.

●  标记元组元素: 元组元素现可被命名为 [start: number, end: number] 的形式。如果命名其中一个元素,则所有元素必须均被命名。

type Foo = [first: number, second?: string, ...rest: any[]];// This allows the arguments to be named correctly here, it also shows up in the editor.declare function someFunc(...args: Foo);

●  从构造函数推断类属性: 在构造函数中设置属性时,现可推断其类型,不再需要手动设置。

class Animal {// No need to set types when they are assigned in the constructor.name;constructor(name: string) {this.name = name;console.log(this.name); // typed as string}}

JSDoc @deprecated 支持: TypeScript 现可识别 JSDoc/TSDoc @deprecated 标签。

/** @deprecated message */type Test = string;const test: Test = 'dfadsf'; // Type error: 'Test' is deprecated.
TypeScript 4.1

●  模板字面量类型: 在定义字面量类型时,可以通过 ${Type}等模板指定类型。这样可以构造复杂的字符串类型,例如将多个字符串字面量组合起来。

type VerticalDirection = 'top' | 'bottom';type HorizontalDirection = 'left' | 'right';type Direction = `${VerticalDirection} ${HorizontalDirection}`;const dir1: Direction = 'top left';const dir2: Direction = 'left'; // Type error: Type '"left"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.const dir3: Direction = 'left top'; // Type error: Type '"left top"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.// This can also be combined with generics and the new utility types.declare function makeId<T extends string, U extends string>(first: T, second: U): `${Capitalize<T>}-${Lowercase<U>}`;

●  在映射类型中重新映射键: 为已映射的类型重新分配类型,但仍使用其值,例如 [K in keyof T as NewKeyType]: T[K]。

// Let's say we wanted to reformat an object but prepend its IDs with an underscore.const obj = { value1: 0, value2: 1, value3: 3 };const newObj: { [Property in keyof typeof obj as `_${Property}`]: number }; // typed as { _value1: number; _value2: number; value3: number; }

●  递归条件类型: 在定义之内使用条件类型,这种类型允许以有条件方式解包无限嵌套值。

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;type P1 = Awaited<string>; // typed as stringtype P2 = Awaited<Promise<string>>; // typed as stringtype P3 = Awaited<Promise<Promise<string>>>; // typed as string

●  JSDOC @see 标签的编辑器支持: JSDoc/TSDoc @see variable/type/link 标签现可在编辑器中受到支持。

const originalValue = 1;/*** Copy of another value* @see originalValue*/const value = originalValue;

●  tsc — explainFiles:  --explainFiles 选项可被 TypeScript CLI 用于解释哪些文件是编译的一部分、为什么会这样。这一点对于调试非常重要。警告:对于大型项目或较为复杂的设置,这会生成大量输出,建议改用 tsc --explainFiles | less 或其他类似功能。

tsc --explainFiles<<output../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es5.d.tsLibrary referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'Library referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.tsLibrary referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'Library referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.tsLibrary referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'Library referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'...output

●  解构变量可被显式标记为未使用: 在解构时,可使用下划线将变量标记为未使用,从而防止 TypeScript 抛出“未使用的变量”错误。

const [_first, second] = [3, 5];console.log(second);// Or even shorterconst [_, value] = [3, 5];console.log(value);
TypeScript 4.3

●  属性上的单独写入类型: 在定义 set/get 访问器时,write/set 类型现可不同于 read/get 类型。意味着设置器能够接受相同值的多种格式。

class Test {private _value: number;get value(): number {return this._value;}set value(value: number | string) {if (typeof value === 'number') {this._value = value;return;}this._value = parseInt(value, 10);}}

●  override: 使用 override,会将继承的类方法显式标记为覆写。因此当父类发生变化时,TypeScript 会提醒父方法已不存在,从而实现更安全的复杂继承模式。

class Parent {getName(): string {return 'name';}}class NewParent {getFirstName(): string {return 'name';}}class Test extends Parent {override getName(): string {return 'test';}}class NewTest extends NewParent {override getName(): string { // Type error: This member cannot have an 'override' modifier because it is not declared in the base class 'NewParent'.return 'test';}}

●  静态索引签名: 在类上使用静态属性时,现在也可以使用 static [propName: string]: string 设置索引签名。

// PREVIOUSLY:class Test {}Test.test = ''; // Type error: Property 'test' does not exist on type 'typeof Test'.// NEW:class NewTest {static [key: string]: string;}NewTest.test = '';

对JSDOC@link标签提供编辑器支持: JSDoc/TSDoc {@link variable/type/link} 内联标签现可在编辑器中显示和解析。

const originalValue = 1;/*** Copy of {@link originalValue}*/const value = originalValue;
TypeScript 4.4

●  精确的可选属性类型 ( — exactOptionalPropertyTypes): 使用编译器标志 --exactOptionalPropertyTypes 时(或在 tsconfig.json 中),隐式允许 undefined(例如 property?: string)的属性将不允许被分配为 undefined。相反,undefined 必须经过明确许可,例如 property: string | undefined。

class Test {name?: string;age: number | undefined;}const test = new Test();test.name = 'test'; // Type error: Option 'exactOptionalPropertyTypes' cannot be specified without specifying option 'strictNullChecks'.test.age = 0;
TypeScript 4.5

●  Awaited 类型与 Promise 改进: 新的 Awaited<>实用程序类型能从无限嵌套的 Promises 中提取值类型(类似于 await 对该值的操作)。这也改进了 Promise.all() 的类型推断。

// Let's say we want to have a generic awaited value.// We can use the Awaited utility type for this (its source code was part of a previous example),// so infinitely nested Promises all resolve to their value.type P1 = Awaited<string>; // typed as stringtype P2 = Awaited<Promise<string>>; // typed as stringtype P3 = Awaited<Promise<Promise<string>>>; // typed as string

●  导入名称上的类型修饰符: 在普通(非 import type)导入语句中,关键字 type 可用于表示该值只应在类型编译时导入(且可以去除)。

// PREVIOUSLY:// The optimal way to import types is to use the `import type` keyword to prevent them from actually being imported after compilation.import { something } from './file';import type { SomeType } from './file';// This needs two import statements for the same file.// NEW:// Now this can be combined into one statement.import { something, type SomeType } from './file';

●  const 断言: 在将常量定义为 as const 时,即可将其准确归类为字面量类型。这项功能有多种用例,可以轻松进行准确分类。此功能还会令对象和数组成为 readonly,防止常量对象发生突变。

// PREVIOUSLY:// The optimal way to import types is to use the `import type` keyword to prevent them from actually being imported after compilation.import { something } from './file';import type { SomeType } from './file';// This needs two import statements for the same file.// NEW:// Now this can be combined into one statement.import { something, type SomeType } from './file';

●  类中各方法的片段补全: 当一个类继承多个方法类型时,编辑器现可为各类型提供建议片段。

TypeScript 4.6

●  索引访问推断改进:当直接在键内直接索引一个类型时,如果该类型位于同一对象上,现在其准确率会更高。这也是 TypeScript 现代化特性的良好体现。

interface AllowedTypes {'number': number;'string': string;'boolean': boolean;}// The Record specifies the kind and value type from the allowed types.type UnionRecord<AllowedKeys extends keyof AllowedTypes> = { [Key in AllowedKeys]:{kind: Key;value: AllowedTypes[Key];logValue: (value: AllowedTypes[Key]) => void;}}[AllowedKeys];// The function logValue only accepts the value of the Record.function processRecord<Key extends keyof AllowedTypes>(record: UnionRecord<Key>) {record.logValue(record.value);}processRecord({kind: 'string',value: 'hello!',// The value used to implicitly have the type string | number | boolean,// but now is correctly inferred to just string.logValue: value => {console.log(value.toUpperCase());}});

●  TypeScript Trace Analyzer ( — generateTrace):  --generateTrace选项可在 TypeScript CLI 当中用于生成一个文件,其中包含关于类型检查和编译过程的详情信息。这有助于优化复杂类型。

tsc --generateTrace tracecat trace/trace.json<<output[{"name":"process_name","args":{"name":"tsc"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},{"name":"thread_name","args":{"name":"Main"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},{"name":"TracingStartedInBrowser","cat":"disabled-by-default-devtools.timeline","ph":"M","ts":...,"pid":1,"tid":1},{"pid":1,"tid":1,"ph":"B","cat":"program","ts":...,"name":"createProgram","args":{"configFilePath":"/...","rootDir":"/..."}},{"pid":1,"tid":1,"ph":"B","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},{"pid":1,"tid":1,"ph":"E","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},{"pid":1,"tid":1,"ph":"X","cat":"program","ts":...,"name":"resolveModuleNamesWorker","dur":...,"args":{"containingFileName":"/..."}},...outputcat trace/types.json<<output[{"id":1,"intrinsicName":"any","recursionId":0,"flags":["..."]},{"id":2,"intrinsicName":"any","recursionId":1,"flags":["..."]},{"id":3,"intrinsicName":"any","recursionId":2,"flags":["..."]},{"id":4,"intrinsicName":"error","recursionId":3,"flags":["..."]},{"id":5,"intrinsicName":"unresolved","recursionId":4,"flags":["..."]},{"id":6,"intrinsicName":"any","recursionId":5,"flags":["..."]},{"id":7,"intrinsicName":"intrinsic","recursionId":6,"flags":["..."]},{"id":8,"intrinsicName":"unknown","recursionId":7,"flags":["..."]},{"id":9,"intrinsicName":"unknown","recursionId":8,"flags":["..."]},{"id":10,"intrinsicName":"undefined","recursionId":9,"flags":["..."]},{"id":11,"intrinsicName":"undefined","recursionId":10,"flags":["..."]},{"id":12,"intrinsicName":"null","recursionId":11,"flags":["..."]},{"id":13,"intrinsicName":"string","recursionId":12,"flags":["..."]},...output

TypeScript 4.7

●  在 Node.js 中支持 ECMAScript 模块: 在使用 ES Modules 替代 CommonJS 时,TypeScript 现可支持指定默认值。具体指定在 tsconfig.json 中实现。

..."compilerOptions": [..."module": "es2020"]...

●  package.json 中的类型:  package.json 中的 type 字段可被设定为"module",以供 ES Modules 使用 node.js。在大多数情况下,这种方式对 TypeScript 已经足够,不需要前面提到的编译器选项。

..."type": "module"...

●  实例化表达式: 实例化表达式允许在引用一个值时,指定类型参数。这样可以在不创建包装器的前提下,收窄泛型类型。

class List<T> {private list: T[] = [];get(key: number): T {return this.list[key];}push(value: T): void {this.list.push(value);}}function makeList<T>(items: T[]): List<T> {const list = new List<T>();items.forEach(item => list.push(item));return list;}// Let's say we want to have a function that creates a list but only allows certain values.// PREVIOUSLY:// We need to manually define a wrapper function and pass the argument.function makeStringList(text: string[]) {return makeList(text);}// NEW:// Using instantiation expressions, this is much easier.const makeNumberList = makeList<number>;

●  扩展对推断类型变量的约束: 在条件类型中推断类型变量时,现在可以使用 extends 直接将其收窄 / 约束。

// Let's say we want to type a type that only gets the first element of an array if it's a string.// We can use conditional types for this.// PREVIOUSLY:type FirstIfStringOld<T> =T extends [infer S, ...unknown[]]? S extends string ? S : never: never;// But this needs two nested conditional types. We can also do it in one.type FirstIfString<T> =T extends [string, ...unknown[]]// Grab the first type out of `T`? T[0]: never;// This is still suboptimal because we need to index the array for the correct type.// NEW:// Using extends Constraints on infer Type Variables, this can be declared a lot easier.type FirstIfStringNew<T> =T extends [infer S extends string, ...unknown[]]? S: never;// Note that the typing worked the same before, this is just a cleaner syntax.type A = FirstIfStringNew<[string, number, number]>; // typed as stringtype B = FirstIfStringNew<["hello", number, number]>; // typed as "hello"type C = FirstIfStringNew<["hello" | "world", boolean]>; // typed as "hello" | "world"type D = FirstIfStringNew<[boolean, number, string]>; // typed as never

●  类型参数的可选变体注释: 泛型在检查是否“匹配”时可以有不同行为,例如对 getter 和 setter,对是否允许继承的判断是相反的。为了明确起见,现在用户可以明确指定。

// Let's say we have an interface / a class that extends another one.interface Animal {animalStuff: any;}interface Dog extends Animal {dogStuff: any;}// And we have some generic "getter" and "setter".type Getter<T> = () => T;type Setter<T> = (value: T) => void;// If we want to find out if Getter<T1> matches Getter<T2> or Setter<T1> matches Setter<T2>, this depends on the covariance.function useAnimalGetter(getter: Getter<Animal>) {getter();}// Now we can pass a Getter into the function.useAnimalGetter((() => ({ animalStuff: 0 }) as Animal));// This obviously works.// But what if we want to use a Getter which returns a Dog instead?useAnimalGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));// This works as well because a Dog is also an Animal.function useDogGetter(getter: Getter<Dog>) {getter();}// If we try the same for the useDogGetter function we will not get the same behavior.useDogGetter((() => ({ animalStuff: 0 }) as Animal)); // Type error: Property 'dogStuff' is missing in type 'Animal' but required in type 'Dog'.// This does not work, because a Dog is expected, not just an Animal.useDogGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));// This, however, works.// Intuitively we would maybe expect the Setters to behave the same, but they don't.function setAnimalSetter(setter: Setter<Animal>, value: Animal) {setter(value);}// If we pass a Setter of the same type it still works.setAnimalSetter((value: Animal) => {}, { animalStuff: 0 });function setDogSetter(setter: Setter<Dog>, value: Dog) {setter(value);}// Same here.setDogSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });// But if we pass a Dog Setter into the setAnimalSetter function, the behavior is reversed from the Getters.setAnimalSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 }); // Type error: Argument of type '(value: Dog) => void' is not assignable to parameter of type 'Setter<Animal>'.// This time it works the other way around.setDogSetter((value: Animal) => {}, { animalStuff: 0, dogStuff: 0 });// NEW:// To signal this to TypeScript (not needed but helpful for readability), use the new Optional Variance Annotations for Type Parameters.type GetterNew<out T> = () => T;type SetterNew<in T> = (value: T) => void;

●  使用 moduleSuffixes 实现分辨率自定义: 在使用具有自定义文件后缀的环境时(例如,.ios 用于原生应用构建),现在您可以为 TypeScript 指定这些后缀以正确对导入进行解析。具体指定在 tsconfig.json 中实现。

..."compilerOptions": [..."module": [".ios", ".native", ""]]...import * as foo from './foo';// This first checks ./foo.ios.ts, ./foo.native.ts, and finally ./foo.ts.

●  在编辑器内转到源定义: 在编辑器中,开放新的“转到源定义”菜单项。其功能类似于“转到定义”,但更多指向.ts 和 .js 文件,而非类型定义 (.d.ts)。

TypeScript 4.9

●  satisfies 运算符: satisfies 运算符允许检查与类型间的兼容性,且无需实际分配该类型。这样可以在保持兼容性的同时,获得更准确的类型推断。

// PREVIOUSLY:// Let's say we have an object/map/dictionary which stores various items and their colors.const obj = {fireTruck: [255, 0, 0],bush: '#00ff00',ocean: [0, 0, 255]} // typed as { fireTruck: number[]; bush: string; ocean: number[]; }// This implicitly types the properties so we can operate on the arrays and the string.const rgb1 = obj.fireTruck[0]; // typed as numberconst hex = obj.bush; // typed as string// Let's say we only want to allow certain objects.// We could use a Record type.const oldObj: Record<string, [number, number, number] | string> = {fireTruck: [255, 0, 0],bush: '#00ff00',ocean: [0, 0, 255]} // typed as Record<string, [number, number, number] | string>// But now we lose the typings of the properties.const oldRgb1 = oldObj.fireTruck[0]; // typed as string | numberconst oldHex = oldObj.bush; // typed as string | number// NEW:// With the satisfies keyword we can check compatibility with a type without actually assigning it.const newObj = {fireTruck: [255, 0, 0],bush: '#00ff00',ocean: [0, 0, 255]} satisfies Record<string, [number, number, number] | string> // typed as { fireTruck: [number, number, number]; bush: string; ocean: [number, number, number]; }// And we still have the typings of the properties, the array even got more accurate by becoming a tuple.const newRgb1 = newObj.fireTruck[0]; // typed as numberconst newRgb4 = newObj.fireTruck[3]; // Type error: Tuple type '[number, number, number]' of length '3' has no element at index '3'.const newHex = newObj.bush; // typed as string

●  编辑器中的“删除未使用的导入”与“排序导入”命令: 在编辑器中,新命令(及自动修复)“删除未使用的导入”和“排序导入”让导入管理更加轻松易行。

原文链接:

https://medium.com/@LinusSchlumberger/all-javascript-and-typescript-features-of-the-last-3-years-629c57e73e42

                    
Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞在看” 支持一下

浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报