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

66e74f25d9f7bb4e70c188c9959da263.jpeg

大厂技术  高级前端  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')]); // undefined
console.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 instance
console.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; // Valid
this.#privateField; // Syntax error
this.#undeclaredField = 444; // Syntax error
console.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); // undefined
console.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')); // true
console.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.type
type 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 number
type Str = GetReturnType<(x: string) => string>; // typed as string
type 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 number
list[1] // typed as number | undefined
list[2] // typed as boolean | undefined
list[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 Test
const 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]); // 23
const 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 string
type P2 = Awaited<Promise<string>>; // typed as string
type 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.ts
Library 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.ts
Library 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.ts
Library 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 shorter
const [_, 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 string
type P2 = Awaited<Promise<string>>; // typed as string
type 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';

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

bb5a55a61f61e472999dbaa746ff784f.gif

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 trace
cat 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":"/..."}},
...
output
cat 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 string
type 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)。

237a9fd8726f4fe434a618d3147e3c42.gif

efb2a28a04d42030c270fa8f9d1f90e8.gif

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 number
const 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 | number
const 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 number
const 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

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

35be65afbaef1fc7b209628ea0fbff1f.gif

原文链接:

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」即可。

e88e9605d564418c431007056967a97a.png

“分享、点赞、在看” 支持一下
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值