likes
comments
collection
share

TS5(TypeScript5) 发布的新功能(2023年3月16日)

作者站长头像
站长
· 阅读数 33

参考资料:Microsoft Announcing TypeScript 5.0

此版本带来了许多新功能,同时旨在使 TypeScript 更小、更简单、更快。 增加了新的装饰器标准,优化了对 Node 和 bundlers 的 ESM 项目的支持,为库作者提供了控制泛型推理的新方法,扩展了 JSDoc 功能,简化了配置,并进行了许多其他改进。

若要开始使用 TypeScript 5.0,可以通过以下命令使用:

npm install -D typescript

在NodeJs中要使用 TypeScript 5.0 最低版本要求至少为 Node.js 12.20 及更高版本。

以下是 TypeScript 5.0 中的新增功能:

  • 全新的装饰器
  • 泛型参数的常量修饰
  • 支持extends合并多个配置文件
  • 枚举增强
  • moduleResolution 配置新增 bundler支持
  • 自定义解析标志
  • 新增类型导入导出指令 --verbatimModuleSyntax
  • 支持导出 export type *
  • JSDoc 新增 @satisfies 支持
  • JSDoc 新增 @overload 支持
  • 运行 tsc --buid 时可以传入的新指令
  • 编辑器中不区分大小写的导入排序
  • switch 语法补足
  • 优化速度、内存和包大小
  • 废弃功能

全新装饰器

装饰器是即将推出的 ECMAScript 功能,它允许我们以可重用的方式自定义类及其成员。说是即将推出,其实NestJs中已经大量使用装饰器语法。

让我们阅读以下代码:

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}

const p = new Person("Ron");
p.greet();

greet这里很简单,假设里面的代码逻辑是非常复杂的比如包含异步逻辑、递归、副作用等等。 那就可能需要使用 console.log 来调试 greet

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    greet() {
        console.log("LOG: Entering method.");

        console.log(`Hello, my name is ${this.name}.`);

        console.log("LOG: Exiting method.")
    }
}

这种方式调试非常普遍,那如果有一种方法可以为每这类型的函数执行此操作,那就方便很多。而这就是装饰师的用武之地。

我们可以编写一个名为loggedMethod的函数,如下所示:

function loggedMethod(originalMethod: any, _context: any) {

    function replacementMethod(this: any, ...args: any[]) {
        console.log("LOG: Entering method.")
        const result = originalMethod.call(this, ...args);
        console.log("LOG: Exiting method.")
        return result;
    }

    return replacementMethod;
}

然后用loggedMethod来装饰 greet函数,如此就能得到如上述一直的 console 输出,但不一样的是,你的 loggedMethod 装饰器完全可以复用,它可以用于装饰其他类的函数方法。

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    @loggedMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}

const p = new Person("Ron");
p.greet();

// Output:
//
//   LOG: Entering method.
//   Hello, my name is Ron.
//   LOG: Exiting method.

我们再看一个例子,通常我们可能会如下这样调用实例的函数。

const greet = new Person("Ron").greet; // We don't want this to fail! 
greet();

这样调用有个通常会产生一个this的使用问题,一般是这样么解决的,在构造函数中为 greet 绑定 this

// class Person {
//     name: string;
//     constructor(name: string) {
//         this.name = name;
        // 在构造函数中为
        this.greet = this.greet.bind(this);
//     }

//     greet() {
//         console.log(`Hello, my name is ${this.name}.`);
//     }
// }

为此我们可以创建一个bound装饰器

function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
    const methodName = context.name;
    if (context.private) {
        throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
    }
    context.addInitializer(function () {
        this[methodName] = this[methodName].bind(this);
    });
}

greet叠加使用装饰器

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    @bound
    @loggedMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}

const p = new Person("Ron");
const greet = p.greet;

greet(); // Hello, my name is Ron

以上都是对装饰器的简单演示使用,在实践中我们可以看看NestJs中的装饰器

例如: ControllerPost Get路由:

import { Controller, Get, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { ConfigService } from '@nestjs/config';

@Controller('user')
export class UserController {
  constructor(
    private userService: UserService,
    private configService: ConfigService,
  ) {}

  @Get()
  getUsers(): any {
    return this.userService.getUsers();
  }

  @Post()
  addUser(): any {
    return this.userService.addUser();
  }
}

例如:依赖注入

// app.module.ts

import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
// user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

// user.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  getUsers() {
    return {
      code: 0,
      data: [],
      msg: '请求用户列表成功!',
    };
  }

  addUser() {
    return {
      code: 0,
      data: {},
      msg: '添加用户成功',
    };
  }
}

例如:TypeROM

import {
  Column,
  Entity,
  JoinColumn,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './User';

@Entity('profile', { schema: 'testdb' })
export class Profile {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
  id: number;

  @Column('int', { name: 'gender' })
  gender: number;

  @Column('varchar', { name: 'photo', length: 255 })
  photo: string;

  @Column('varchar', { name: 'address', length: 255 })
  address: string;

  @OneToOne(() => User, (user) => user.profile, {
    onDelete: 'NO ACTION',
    onUpdate: 'NO ACTION',
  })
  @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }])
  user: User;
  
  @OneToOne(() => Profile, (profile) => profile.user)
  profile: Profile;

  @ManyToMany(() => Roles, (roles) => roles.users)
  roles: Roles[];
  
  @ManyToMany(() => User, (user) => user.roles)
  @JoinTable({
    name: 'users_roles',
    joinColumns: [{ name: 'rolesId', referencedColumnName: 'id' }],
    inverseJoinColumns: [{ name: 'userId', referencedColumnName: 'id' }],
    schema: 'testdb',
  })
  users: User[];
}

泛型参数的常量修饰

假设我们有这样一个函数,参数为泛型。

declare function foo<T>(x: T): T;

我们调用这个函数,

foo({
    title: 'a标签状态'
    names: ["active", "hover", "visited", "link"]
})

这时候TS对泛型 T 的类型推断结果为。
{
    title: string
    names: string[]
}

但是如果我对于names的定义是个常量呢,那上面的推断结果并不符合我的需求。这时候我们就可以使用 const修饰

declare function foo<const T>(x: T): T;

foo({
    title: 'a标签状态'
    names: ["active", "hover", "visited", "link"]
})

这时候TS对泛型 T 的类型推断结果为。
{
    title: 'a标签状态'
    names: ["active", "hover", "visited", "link"]
}

// 如果不使用常量修饰,等价于此效果
foo({
    title: 'a标签状态'
    names: ["active", "hover", "visited", "link"]
} as const);

多个配置文件

可以支持合并多个tsconfig配置文件。

// tsconfig.json
{
    "extends": [
        "@tsconfig/strictest/tsconfig.base.json",
        "@tsconfig/strictest/tsconfig.extend.json",
        "@tsconfig/strictest/tsconfig.more.json"
    ],
    "compilerOptions": {
        // ...
    }
}

枚举增强

所有的枚举都是联合类型,以下的枚举错误,在新版本中都可正确使用。 实例说明:

const num = 1
const str = 'str'

// str 在后面并且没有指定值,会提示错误: --枚举成员必须具有初始化表达式
// ts5 已经支持,并且会进行累加值处理
enum Enum1 {
  Index = num,
  str,
}

// 字符串计算,会提示错误: --只有数字枚举可具有计算成员,但此表达式的类型为“string”。如果不需要全面性检查,请考虑改用对象文本
enum Enum2 {
  str = `abc_${str}`,
}

// const 修饰的常量枚举赋值变量时,会提示错误: --常量枚举成员初始值设定项只能包含字面量值和其他计算的枚举值
const enum Enum3 {
  Index = num,
}

// const 修饰的常量枚举赋值计算时,会提示错误: --常量枚举成员初始值设定项只能包含字面量值和其他计算的枚举值
const enum Enum4 {
  str = `abc_${str}`,
}


// 修复多个enum类型组成的混合类型都会变成 number 类型的问题
enum Enum5 {
  str = "stra"
} 
enum Enum6 {
    num = 1,
    str = Enum5.str
}
// 如下代码,在5.0 版本中会提示错误: -- 类型 Enum6 不可分配给类型 number
const a: number = Enum6.str;

moduleResolution 配置项

TypeScript 4.7 为配置项compilerOption.modulecompilerOption.moduleResolution  新增了 node16 和 nodenext 选项,以改进 TS + NodeJs + ESM 的开发体验,使得在NodeJs中能更好的支持 ESM 标准。

注意NodeJs中后缀名为.cjs的文件视为 CommonJS 模块,后缀为.mjs文件视为 ECMAScript 模块。使用TS时相对应为 .ctsmts

但是在这种模式下,会有很多限制,比如在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名:

import * as utils from "./utils"; // 错误-提示需要包括文件扩展名

import * as utils from "./utils.mjs"; // 可以正常工作

这样做虽然可以提升编译时的文件查找速度,但是对于现代构建打包构建工具与代码习惯来说就诸多不便了。 因此新增了 bundler 的配置选项,他能同时兼容 ESM 和 CommonJs 标准同时也不会有上面提到的限制。

bundler相关的细化配置项

allowImportingTsExtensions

启用配置后,在相对导入时就允许使用特定于 TypeScript 的扩展名.ts.mts.tsx

使用allowImportingTsExtensions时注意要同时启用 --noEmit 或者 --emitDeclarationOnly,这是因为这些文件导入路径还需要被构建工具进行处理后才能正常使用。

resolvePackageJsonExports 与 resolvePackageJsonImports

启用配置后,import来自node_modules中的模块时,TypeScript 会去解析模块对应的package.json中的exports和imports字段。

注意,当配置项moduleResolutionnode16nodenext 、bundler时,会默认启用

allowArbitraryExtensions

启用配置后,我们知道常见的JavaScript 或 TypeScript文件扩展名一般为.js|.ts|.tsx|.jsx等,如果导入文件模块,不是这种常见的后缀名时,将会以{file basename}.d.{extension} 的形式查找该路径的声明文件。

例如导入 style.css 时将尝试加载 style.d.css.ts 声明文件。 官方例子:

/* style.css */
.app-container {}
.app-main-title {}
// style.d.css.ts
declare const css: {
  appContainer: string;
  appMainTitle: string;
};

export default css;

customConditions

--customConditions 获取当 TypeScript 从 package.jsonexports (nodejs.org/api/package…) 或 imports 字段解析时应该成功的附加的条件列表。这些条件将添加到解析器默认使用的现有条件中。

例如,当此字段在 tsconfig.json 中设置为:

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

任何时候在 package.json 中引用 exports 或 imports 字段时,TypeScript 都会考虑名为 my-condition 的条件。

因此,当从具有以下 package.json 的包中导入时,TypeScript 将尝试查找与foo.mjs对应的文件。

{
    // ...
    "exports": {
        ".": {
            "my-condition": "./foo.mjs",
            "node": "./bar.mjs",
            "import": "./baz.mjs",
            "require": "./biz.mjs"
        }
    }
}

verbatimModuleSyntax

默认情况下,以下导入TypeScript 会测到你只是对类型使用导入,所以输出结果会将此导入代码删除。

import { Car } from "./car"; 

export function drive(car: Car) { // ... }
// TypeScript 处理后的结果
export function drive(car) { // ... }

配置驱动后,参考以下的导入导出经 TypeScript 处理后的结果说明:

// 删除
import type * as car from "./car";

// 删除
import { type Car } from "./car";
export { type Car } from "./car";

// 完全被删除
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";

全量导出 export type *

可以使用以下两种方式全量导出类型

export * from "module"
export * as ns from "module"

JSDoc @satisfies

@satisfies 使得我们能指定更精准类型提示信息 使用方式:

// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 */

/**
 * @typedef ConfigSettings
 * @prop {CompilerOptions} [compilerOptions]
 * @prop {string | string[]} [extends]
 */


/**
 * @satisfies {ConfigSettings}
 */
let myConfigSettings = {
  compilerOptions: {
      strict: true,
      outDir: "../lib",
  },
  extends: [
      "@tsconfig/strictest/tsconfig.json",
      "../../../tsconfig.base.json"
  ],
};

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

JSDoc @overload

@overload可以在函数传入参数的数据类型不同时的提示不同的类型声明信息。 这个新标签的实现要归功于Tomasz Lenarcik

// @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);
}

tsc --buid 时可以传入的新指令

  • --declaration
  • --emitDeclarationOnly
  • --declarationMap
  • --sourceMap
  • --inlineSourceMap

更多详细相关信息查阅

编辑器中的不区分大小写的导入排序

在Visual Studio和VS Code等编辑器中,TypeScript提供了组织和排序导入和导出的体验。

TypeScript 以前认为导入列表要排序,然而更多的人不喜欢区分大小,ESLing等工具默认情况下需要不区分大小写。这就就容易产生冲突。

switch 语法补足

在编写switch语句时,TypeScript 会检测被检查的值如果是字面量类型,则会提供代码块自动补全输入代码。您可以在 GitHub 上查看实现的细节TS5(TypeScript5) 发布的新功能(2023年3月16日)

弃用和默认更改

在 TypeScript 5.0 中,将计划弃用以下设置和设置值,现在继续使用这些配置项会有警告提示,直到TypeScript5.5版本才会正式删除。

  • --target: ES3
  • --out
  • --noImplicitUseStrict
  • --keyofStringsOnly
  • --suppressExcessPropertyErrors
  • --suppressImplicitAnyIndexErrors
  • --noStrictGenericChecks
  • --charset
  • --importsNotUsedAsValues
  • --preserveValueImports
  • prepend

--newLine 代码行结束时的换行符,新的默认值是LF

--forceConsistentCasingInFileNames 它确保了项目中对同一文件名的所有引用都以大小写形式达成一致,现在默认为true。这可以帮助发现在不区分大小写的文件系统上编写的代码的差异问题。

转载自:https://juejin.cn/post/7213193986345222199
评论
请登录