Typescript 5 来了

语言: CN / TW / HK

## 背景

前段时间发布了 Typescript 5.0 beta 版,预计3月14号发布正式版,我们来一起看看有哪些新特性。

## 装饰器

TS5 支持的是 Stage3 装饰器,进入到 Stage3 的特性基本上就可以认为可以加入 JS 标准了,更多内容可以看下之前我整理的文档:再来了解一下装饰器

## const 类型参数

Typescript 通常会把一个对象推断成一个更通用的类型。比如下面的例子,推断出的 names 是 string[];

```JavaScript type HasNames = { names: readonly string[] }; function getNamesExactly(arg: T): T["names"] { return arg.names; }

// 推断的类型是: string[] const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]}); ```

假如我们希望推断的类型是 ["Alice", "Bob", "Eve"],就需要用 as const 转化一下;

```JavaScript

// 推断的类型是 ["Alice", "Bob", "Eve"] const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const); ```

这种方式用起来不够优雅而且可能会忘记加,所以 TS5 的类型参数支持了 const 描述符;

```JavaScript type HasNames = { names: readonly string[] }; function getNamesExactly(arg: T): T["names"] { return arg.names; }

// 推断类型: readonly ["Alice", "Bob", "Eve"] // 这样就不需要 as const 了 const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] }); ```

不过需要注意的是,如果约束是可变类型,会存在一些问题;

什么是可变类型?举个例子:Array vs ReadonlyArray,前者是可变类型,后者是不可变类型

```JavaScript declare function fnBad(args: T): void;

// T 仍然是 string[],因为 readonly ["a", "b", "c"] 不能赋值给 string[] fnBad(["a", "b" ,"c"]); ```

本来推断出的 T 应该是 readonly ["a", "b", "c"],但是 readonly ["a", "b", "c"] 不能赋值给 string[],所以 T 的类型回退成了 string[]

解决的办法,也很简单,使用 readonly string[] 替换 string[]

```JavaScript declare function fnGood(args: T): void;

// T 是 readonly ["a", "b", "c"] fnGood(["a", "b" ,"c"]); ```

还有一点需要注意,const 修饰符只能影响直接写在函数调用中的对象、数组和原始表达式的推断,举个例子:

```JavaScript declare function fnGood(args: T): void;

const arr = ["a", "b" ,"c"]; // T 仍然是 string[] -- const 修饰符在这里没有任何效果 fnGood(arr); // T 是 readonly string ["a", "b" ,"c"] fnGood(["a", "b" ,"c"]); ```

## 所有枚举都是 union 枚举

TS 最初设计的枚举类型比较简单,除了 E.Foo and E.Bar 只能赋值给 E 类型的变量之外,这些枚举类型的成员其实就是数字。

JavaScript enum E { Foo = 10, Bar = 20, }

TS2.0 引入了枚举字面量,它给每个成员都分配了一个类型,那么这个枚举类型就变成了由所有成员类型组成的 union 类型,我们把它叫做 union enum

JavaScript // Color 就类似于一个 Union类型:Red | Orange | Yellow | Green | Blue | Violet enum Color { Red, Orange, Yellow, Green, Blue, Violet }

union enum 的优势在于,我们可以使用这个枚举类型的子集。如下,Color 包含六个成员,我们可以定义包含三个成员的子集类型。

```JavaScript enum Color { Red, Orange, Yellow, Green, Blue, Violet }

// 每个枚举成员都有自己的类型 // 定义一个只包含三个成员的 Union 类型,相当于 type PrimaryColor = Color.Red | Color.Green | Color.Blue; ```

但是枚举成员的类型与枚举成员的值是强相关的。如果枚举成员的值是函数,就无法计算出成员的值,所以就没办法给每个成员分配对应的类型,也就没办法将枚举转变成 union enum

TS5 解决了这个问题,即使成员的值是函数,也能为其创建唯一的类型;每个枚举类型都是 union enum

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8bebb8a217fc4c1c8cda98ba895de1f9~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e1108c0d06394620bf3daa1b65f22c31~tplv-k3u1fbpfcp-watermark.image?)

## 枚举类型的两个新报错

  1. 给枚举变量赋值成员值以外的值时会报错

```JavaScript enum SomeEvenDigit { Zero = 0, Two = 2, Four = 4 }

// 错误,1 不是成员的值 let m: SomeEvenDigit = 1; ```

  1. 成员值是 string/number 混合并且存在间接赋值的场景

```JavaScript enum Letters { A = "a" } enum Numbers { one = 1, two = Letters.A }

// 错误 const t: number = Numbers.two; ```

## 支持 export type *

TS3.8 支持了针对 type 的导入,TS5 在此基础上扩展出了 export * from "module" 或者 export * as ns from "module"

```TypeScript // models/vehicles.ts export class Spaceship { // ... }

// models/index.ts export type * as vehicles from "./spaceship";

// main.ts import { vehicles } from "./models";

function takeASpaceship(s: vehicles.Spaceship) { // 这里没问题 - vehicles 只能当成类型使用 }

function makeASpaceship() { return new vehicles.Spaceship(); // ^^^^^^^^ // vehicles 不能当成值使用,因为它是通过 export type 导出的. } ```

## JSDoc 支持 @satisfies

TS4.9 引入了 satisfies 操作符,它可以保证变量兼容某个类型,比如

```TypeScript interface NewType { name: string; hobby: string | string[] }

/ * a 被推断为 * { * name: string; * hobby: string; . } / let a: NewType = { name: 'name1', hobby: 'one hobby' };

(hobby as string).toLower ```

TS5 让 JSDoc 上也支持了 satisfies

```TypeScript // @ts-check

/* * @typedef NewType * @prop {string} [name] * @prop {string | string[]} [hobby] /

/* * @satisfies {NewType} / let a = { name: 'name1', hobby: 'one hobby' };

```

## JSDoc 支持 @overload

JSDoc 通过 @overload 支持函数重载。

TS

```TypeScript // 函数重载: function printValue(str: string): void; function printValue(num: number, maxFractionDigits?: number): void;

// 函数定义: function printValue(value: string | number, maximumFractionDigits?: number) { if (typeof value === "number") { const formatter = Intl.NumberFormat("en-US", { maximumFractionDigits, }); value = formatter.format(value); }

console.log(value);

} ```

JSDoc

```TypeScript // @ts-check

/* * @overload * @param {string} value * @return {void} /

/* * @overload * @param {number} value * @param {number} [maximumFractionDigits] * @return {void} /

/* * @param {string | number} value * @param {number} [maximumFractionDigits] / function printValue(value, maximumFractionDigits) { if (typeof value === "number") { const formatter = Intl.NumberFormat("en-US", { maximumFractionDigits, }); value = formatter.format(value); }

console.log(value);

} ```

## extends 支持多个配置文件

当维护多个项目时,通常每个项目的 tsconfig.json 都会继承于一份基准配置。为了提高 extends 的灵活性,TS5 支持集成多个配置文件。

```JavaScript // tsconfig1.json { "compilerOptions": { "strictNullChecks": true } }

// tsconfig2.json { "compilerOptions": { "noImplicitAny": true } }

// tsconfig.json { "extends": ["./tsconfig1.json", "./tsconfig2.json"], "compilerOptions": { },
"files": ["./index.ts"] } ```

## customConditions

假设有一个第三方包的 package.json 包含如下代码:

TypeScript { // ... "exports": { "my-condition": "./foo.mjs", "node": "./bar.mjs", // 用于 node 环境 "import": "./baz.mjs", // 通过 import/import() 引入时使用 "require": "./biz.mjs" // 通过 require 引入时使用 } }

并且你项目中的 tsconfig.json 是这样

TypeScript { "compilerOptions": { "target": "es2022", "moduleResolution": "bundler", "customConditions": ["my-condition"] } }

此时,如果在你项目中 import 了这个第三方包,实际导入的是这个入口 foo.mjs。

## 关系型运算符禁止隐式类型转换

TS5 之前已经禁止了算数运算符的隐式转换,下面的代码会有报错

JavaScript // TS5 之前和之后都会报错 function func(ns: number | string) { return ns * 4; // 错误, 可能存在隐式转换 }

TS5 新增了禁止关系运算符中的隐式类型转换

```JavaScript function func(ns: number | string) { return ns > 4; // 错误, 可能存在隐式转换 }

// 需要做一次显示类型转换 function func(ns: number | string) { return +ns > 4; // 正确 } ```

## 其他
  1. switch/case 自动补全 value 的所有未覆盖的字面量类型;
![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/22c66aedeb6c4f548d51f7206b97e136~tplv-k3u1fbpfcp-watermark.image?)
  1. 针对 --build 可以指定特定产物的标志,比如:打包产物需要包含类型声明文件tsc --build -p ./my-project-dir --declaration

  2. --verbatimModuleSyntax 简化在编译产物里对于 import 的剔除策略

```TypeScript

// 在编译产物里,整体剔除 import type { A } from "a";

// 在编译产物里改写成 'import { b } from "bcd";' import { b, type c, type d } from "bcd";

// 在编译产物里改写成 'import {} from "xyz";' import { type xyz } from "xyz"; ```

  1. --moduleResolution为了支持更多打包场景,在 node16/nodenext 基础上,新增了 bundler;

  2. 编译速度、打包体积都有很明显的优化。

## 如何体验 TS5?

第一步:通过 npm 安装 ts beta:

TypeScript npm install typescript@beta

第二步:安装 VSCode 扩展:JavaScript and TypeScript Nightly - Visual Studio Marketplace

第三步:在 VSCode 中选择 TS 版本(Command + Shift + P

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/081e305e40d54543b0f51f18c13932f0~tplv-k3u1fbpfcp-watermark.image?)

## 参考

announcing-typescript-5-0-beta

satisfies:dev.to