三年回顧:JavaScript與TypeScript最新特性匯總

大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
本文涵蓋了過去三年中發(fā)布的最新 JavaScript 和 TypeScript 特性,包括異步迭代器、nullish 合并、可選鏈、私有字段等等。對于每個特性,文章提供了簡單的解釋、示例代碼以及使用場景,以便開發(fā)者能夠更好地理解和應(yīng)用這些新特性。同時,文章還介紹了如何在項目中使用這些特性,以及如何通過 polyfill 或者 Babel 等工具使舊的瀏覽器支持這些新特性。
本文將帶大家回顧過去三年(乃至更早)以來,JavaScript/ECMAScript 以及 TypeScript 經(jīng)歷的一系列功能變化。
當然,這里提到的很多功能也許跟大家的日常工作八竿子打不著。但關(guān)注功能的發(fā)展變化,應(yīng)該能幫助您加深對這些語言的理解。
有很多 TypeScript 功能并沒有被記錄在內(nèi),因為它們總體上可以概括為“之前它的運作效果跟期望不同,但現(xiàn)在相同了”。所以如果大家之前對某些問題有所詬病,現(xiàn)在不妨重試一次。
● JavaScript / ECMAScript (按時間排序)
● TypeScript (按時間排序)
● 標記模板字面量:通過在模板字面量之前添加函數(shù)名,可以將函數(shù)傳遞至模板字面量和模板值的某些部分。這項功能有不少有趣的用途。
// 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。內(nèi)部使用。
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"}
● 可選鏈:要訪問一個可能未定義的對象的值(通過索引),可以通過在父對象名稱后添加? 來使用可選鏈。可選鏈也可用于索引 ([...]) 或者函數(shù)調(diào)用。
// 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(): 動態(tài)導(dǎo)入,例如 import ... from ...,但在運行上且使用變量。
let importModule;if (shouldImport) {importModule = await import('./module.mjs');}
● String.matchAll: 獲取正則表達式的多個匹配項,包括其捕獲組,且不使用循環(huán)。
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: 在全局上下文中訪問變量,與環(huán)境無關(guān)(瀏覽器、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 …: 輕松將默認值重新導(dǎo)出為子模塊。
export * as am from 'another-module'import { am } from 'module'
● String.replaceAll(): 替換掉某字符串內(nèi)某一子字符串的所有實例,無需始終使用帶有全局標志(/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 列表中的一個結(jié)果時,則返回第一個結(jié)果;僅在所有 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 或可以轉(zhuǎn)換為 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 或轉(zhuǎn)換為 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.
● 數(shù)字分隔符 (_): 使用 _ 分隔數(shù)字以提高可讀性。不會對功能造成影響。
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;
● #private: 通過以 # 開頭的命名,使類成員(屬性和方法)私有,即只能通過類本身進行訪問。其無法被刪除或動態(tài)分配。任何不正確行為都會導(dǎo)致 JavaScript(注意,不是 TypeScript)語法錯誤。不推薦在 TypeScript 項目中這樣做,而應(yīng)用直接使用 private 關(guān)鍵字。
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
● 靜態(tài)類成員: 將任意類字段(屬性和方法)標記為靜態(tài)。
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
● 類中的靜態(tài)初始化塊: 類初始化時運行的塊,基本屬于靜態(tài)成員的“構(gòu)造函數(shù)”。
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"
● 導(dǎo)入斷言(非標準,在 V8 中實現(xiàn)):以 import ... from ... assert { type: 'json' }的形式對導(dǎo)入類型做斷言,可用于在不解析 JSON 的前提下將其導(dǎo)入。
import json from './foo.json' assert { type: 'json' };console.log(json.answer); // 42
● RegExp 匹配索引:獲取正則表達式匹配和捕獲組的開始和結(jié)束索引。適用于 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)): 在索引數(shù)組或字符串時,可以使用 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): 現(xiàn)在可以為錯誤指定可選原因,允許在重新拋出時指定原始錯誤。
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}
● Auto-Accessor: 自動將屬性設(shè)為私有,并為其創(chuàng)建 get/set 訪問器。
class Person {accessor name: string;constructor(name: string) {this.name = name;console.log(this.name) // 'test'}}const person = new Person('test');
● 泛型: 將類型傳遞至其他類型,負責在對類型進行泛化后仍保證類型安全。應(yīng)始終優(yōu)先使用泛型,而非 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.
● 條件類型: 根據(jù)某種類型是否匹配 / 擴展另一種類型,來對類型做有條件設(shè)置。可以按照 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
● 使用條件類型進行推斷: 并非所有泛型類型都需要由用戶指定,有些也可以從代碼中推斷得出。要實現(xiàn)基于類型推斷的條件邏輯,必須有 infer 關(guān)鍵字,它會以某種方式定義臨時推斷類型變量。
// 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"
● 構(gòu)造函數(shù)簽名: 在類聲明之外,定義構(gòu)造函數(shù)的類型。在大多數(shù)情況下不應(yīng)使用,建議用抽象類代替。
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 輔助函數(shù),能夠根據(jù)構(gòu)造函數(shù)類型(但不是類)獲取構(gòu)造函數(shù)參數(shù)。
// 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
● 可變元組類型: 元組中的其余元素現(xiàn)在是通用的,且允許使用多個其余元素。
// 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'.
● 標記元組元素: 元組元素現(xiàn)可被命名為 [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);
● 從構(gòu)造函數(shù)推斷類屬性: 在構(gòu)造函數(shù)中設(shè)置屬性時,現(xiàn)可推斷其類型,不再需要手動設(shè)置。
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 現(xiàn)可識別 JSDoc/TSDoc @deprecated 標簽。
/** @deprecated message */type Test = string;const test: Test = 'dfadsf'; // Type error: 'Test' is deprecated.
● 模板字面量類型: 在定義字面量類型時,可以通過 ${Type}等模板指定類型。這樣可以構(gòu)造復(fù)雜的字符串類型,例如將多個字符串字面量組合起來。
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; }
● 遞歸條件類型: 在定義之內(nèi)使用條件類型,這種類型允許以有條件方式解包無限嵌套值。
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 標簽現(xiàn)可在編輯器中受到支持。
const originalValue = 1;/*** Copy of another value* @see originalValue*/const value = originalValue;
● tsc — explainFiles: --explainFiles 選項可被 TypeScript CLI 用于解釋哪些文件是編譯的一部分、為什么會這樣。這一點對于調(diào)試非常重要。警告:對于大型項目或較為復(fù)雜的設(shè)置,這會生成大量輸出,建議改用 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
● 解構(gòu)變量可被顯式標記為未使用: 在解構(gòu)時,可使用下劃線將變量標記為未使用,從而防止 TypeScript 拋出“未使用的變量”錯誤。
const [_first, second] = [3, 5];console.log(second);// Or even shorterconst [_, value] = [3, 5];console.log(value);
● 屬性上的單獨寫入類型: 在定義 set/get 訪問器時,write/set 類型現(xiàn)可不同于 read/get 類型。意味著設(shè)置器能夠接受相同值的多種格式。
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,會將繼承的類方法顯式標記為覆寫。因此當父類發(fā)生變化時,TypeScript 會提醒父方法已不存在,從而實現(xiàn)更安全的復(fù)雜繼承模式。
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';}}
● 靜態(tài)索引簽名: 在類上使用靜態(tài)屬性時,現(xiàn)在也可以使用 static [propName: string]: string 設(shè)置索引簽名。
// 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} 內(nèi)聯(lián)標簽現(xiàn)可在編輯器中顯示和解析。
const originalValue = 1;/*** Copy of {@link originalValue}*/const value = originalValue;
● 精確的可選屬性類型 ( — exactOptionalPropertyTypes): 使用編譯器標志 --exactOptionalPropertyTypes 時(或在 tsconfig.json 中),隱式允許 undefined(例如 property?: string)的屬性將不允許被分配為 undefined。相反,undefined 必須經(jīng)過明確許可,例如 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;
● 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
● 導(dǎo)入名稱上的類型修飾符: 在普通(非 import type)導(dǎo)入語句中,關(guān)鍵字 type 可用于表示該值只應(yīng)在類型編譯時導(dǎo)入(且可以去除)。
// 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 時,即可將其準確歸類為字面量類型。這項功能有多種用例,可以輕松進行準確分類。此功能還會令對象和數(shù)組成為 readonly,防止常量對象發(fā)生突變。
// 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';
● 類中各方法的片段補全: 當一個類繼承多個方法類型時,編輯器現(xiàn)可為各類型提供建議片段。

● 索引訪問推斷改進:當直接在鍵內(nèi)直接索引一個類型時,如果該類型位于同一對象上,現(xiàn)在其準確率會更高。這也是 TypeScript 現(xiàn)代化特性的良好體現(xiàn)。
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
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
● 在 Node.js 中支持 ECMAScript 模塊: 在使用 ES Modules 替代 CommonJS 時,TypeScript 現(xiàn)可支持指定默認值。具體指定在 tsconfig.json 中實現(xiàn)。
..."compilerOptions": [..."module": "es2020"]...
● package.json 中的類型: package.json 中的 type 字段可被設(shè)定為"module",以供 ES Modules 使用 node.js。在大多數(shù)情況下,這種方式對 TypeScript 已經(jīng)足夠,不需要前面提到的編譯器選項。
..."type": "module"...
● 實例化表達式: 實例化表達式允許在引用一個值時,指定類型參數(shù)。這樣可以在不創(chuàng)建包裝器的前提下,收窄泛型類型。
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>;
● 擴展對推斷類型變量的約束: 在條件類型中推斷類型變量時,現(xiàn)在可以使用 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
● 類型參數(shù)的可選變體注釋: 泛型在檢查是否“匹配”時可以有不同行為,例如對 getter 和 setter,對是否允許繼承的判斷是相反的。為了明確起見,現(xiàn)在用戶可以明確指定。
// 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 實現(xiàn)分辨率自定義: 在使用具有自定義文件后綴的環(huán)境時(例如,.ios 用于原生應(yīng)用構(gòu)建),現(xiàn)在您可以為 TypeScript 指定這些后綴以正確對導(dǎo)入進行解析。具體指定在 tsconfig.json 中實現(xiàn)。
..."compilerOptions": [..."module": [".ios", ".native", ""]]...import * as foo from './foo';// This first checks ./foo.ios.ts, ./foo.native.ts, and finally ./foo.ts.
● 在編輯器內(nèi)轉(zhuǎn)到源定義: 在編輯器中,開放新的“轉(zhuǎn)到源定義”菜單項。其功能類似于“轉(zhuǎn)到定義”,但更多指向.ts 和 .js 文件,而非類型定義 (.d.ts)。


● 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
● 編輯器中的“刪除未使用的導(dǎo)入”與“排序?qū)搿泵? 在編輯器中,新命令(及自動修復(fù))“刪除未使用的導(dǎo)入”和“排序?qū)搿弊寣?dǎo)入管理更加輕松易行。

原文鏈接:
https://medium.com/@LinusSchlumberger/all-javascript-and-typescript-features-of-the-last-3-years-629c57e73e42
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點贊、在看” 支持一下
