Typescript 5 来了

16,153 阅读6分钟

背景

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

装饰器

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

const 类型参数

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

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

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

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


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

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

type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
    return arg.names;
}

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

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

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

declare function fnBad<const T extends string[]>(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[]

declare function fnGood<const T extends readonly string[]>(args: T): void;

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

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

declare function fnGood<const T extends readonly string[]>(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 类型的变量之外,这些枚举类型的成员其实就是数字。

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

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

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

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

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

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

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

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

image

image

枚举类型的两个新报错

  1. 给枚举变量赋值成员值以外的值时会报错
enum SomeEvenDigit {
    Zero = 0,
    Two = 2,
    Four = 4
}

// 错误,1 不是成员的值
let m: SomeEvenDigit = 1;
  1. 成员值是 string/number 混合并且存在间接赋值的场景
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"

// 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 操作符,它可以保证变量兼容某个类型,比如

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

// @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

// 函数重载:
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

// @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 支持集成多个配置文件。

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

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

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

customConditions

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

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

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

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

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

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

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

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

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

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

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

其他

  1. switch/case 自动补全 value 的所有未覆盖的字面量类型;

image

  1. 针对 --build 可以指定特定产物的标志,比如:打包产物需要包含类型声明文件tsc --build -p ./my-project-dir --declaration

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




// 在编译产物里,整体剔除
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:

npm install typescript@beta

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

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

image

参考

announcing-typescript-5-0-beta

satisfies:dev.to

欢迎一起交流。

扫码_搜索联合传播样式-标准色版.png