Typescript 5.4
在闭包中保留上次分配的收窄类型
当参数和let变量用在non-hoisted函数中时,类型检查器会去搜寻上一个变量分配类型的地方。如果能找到,就能安全地从外部函数中缩小类型范围。即下面的例子在5.4版本中可用:
function getUrls (url: string | URL, names: string[]) {
if (typeof url === 'string') {
url = new URL(url);
}
return names.map(name => {
// 5.4+开始,能够正确从闭包外部获取到上一次url收窄的类型
url.searchParams.set('name', name)
return url.toString()
})
}
注意,在嵌套函数内分配变量类型,不会启动类型收窄分析,因为无法确认该嵌套函数之后是否会被调用。
实用类型NoInfer
在调用泛型函数时,typescript并不总是能够推断出最佳的类型,这时可能会导致typescript拒绝合理的调用、接收有问题的调用,或是在捕获错误时仅报告更糟糕的错误消息。例如以下代码:
// 期望:defaultColor继承自colors中的元素
function createStreetLight<C extends string>(colors: C[], defaultColor?:C) {}
// 此处能够正常运行,但是和我们的目的是不一致的
createStreetLight(['red', 'yellow', 'green'], 'blue')
改进(如果泛型参数D只在函数签名中使用一次,那么D则是一个code smell):
function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) {}
// error: Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.
createStreetLight(["red", "yellow", "green"], "blue");
使用5.4新增的类型NoInfer,会从NoInfer<T>内部包裹的类型T中推理出合适的类型:
function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {}
// error: Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.
createStreetLight(["red", "yellow", "green"], "blue");
Object.groupBy和Map.groupBy
5.4版本新增对可迭代对象进行分组的两个函数,语法为:Object.groupBy(items, callbackFn),即使用回调函数callbackFn,对可迭代对象items进行分组,然后返回一个分组后的对象,Map类似。例子如下:
const array = [0, 1, 2, 3, 4, 5]
// 回调函数的参数:num: 可迭代对象的元素,index:可迭代对象元素对应的索引
// 回调函数的返回值:对象的键,如果num符合该键对应的条件,则该num归于该键的元素中
const myObj = Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? 'even' : 'odd'
})
// 上述myObj的值为:{ even: [0, 2, 4], odd: [1, 3, 5] }
在--moduleResolution bundler和--module preserve中支持require()调用
5.4版本中,下面例子不报错:
import myModule = require('module/path')
导入属性和断言检查
5.4版本会根据全局的ImportAtributes类型检查导入的属性和断言,这意味着能够更准确描述导入属性:
// 全局文件
interface ImportAttributes {
type: 'json'
}
// 其他模块内,这里的with通常用来描述导入文件的类型(见5.3版本)
// error!
// Type '{ type: "not-json"; }' is not assignable to type 'ImportAttributes'.
// Types of property 'type' are incompatible.
// Type '"not-json"' is not assignable to type '"json"'.
import * as ns from 'foo' with { type: 'not-json' }
编辑器插件/演练场添加缺失参数的快速修复
TypeScript: 文档 - TypeScript 5.4 --- TypeScript: Documentation - TypeScript 5.4 (typescriptlang.org)
Typescript 5.3
导入属性with
5.3版本开始,支持最新更新的导入属性建议,该属性主要是向运行时提供模块(文件)预期格式的信息。比如JSON文件:
import obj from './something.json' with { type: 'json' }
// 或者是动态导入:
const obj = await import('./something.json', { with: { type: 'json' }})
在导入类型中提供对resolution-mode的稳定支持
// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" with {
"resolution-mode": "require"
};
// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" with {
"resolution-mode": "import"
};
export interface MergedType extends TypeFromRequire, TypeFromImport {}
// 动态导入语法:
export type TypeFromRequire =
import("pkg", { with: { "resolution-mode": "require" } }).TypeFromRequire;
export type TypeFromImport =
import("pkg", { with: { "resolution-mode": "import" } }).TypeFromImport;
export interface MergedType extends TypeFromRequire, TypeFromImport {}
在所有模块模式(module modes)中支持resolution-mode
之前仅允许在node16和nodenext中使用该选项,现在可以在moduleResolution选项的所有其他值中使用并正常工作,比如bundler, node, classic
switch(true)收窄
5.3版本开始在switch(true)下的每个case分支下也能够进行类型收窄:
function f(x: unknown) {
switch(true) {
case typeof x === 'string':
// x: string
console.log(x.toUpperCase())
break
case Array.isArray(x):
// x: any[]
console.log(x.length)
break
default:
// x: unknown
和boolean值比较时类型收窄
5.3版本开始,在条件语句中使用类型谓词函数和boolean进行比较(通常情况下都是不必要的,因为可以直接使用,不需要再和boolean进行比较),将能够正常工作:
interface A {
a: string
}
interface B {
b: string
}
type MyType = A | B
function isA(x: MyType): x is A {
return 'a' in x
}
function someFn(x: MyType) {
// 如果不使用=== true,之前版本是可以工作的
// 5.3开始,使用===true,也能够工作
if (isA(x) === true) {
// 正常工作
console.log(x.a)
}
}
通过Symbol.hasInstance对instanceof收窄
5.3版本开始,可将[Symbol.hasInstance]声明为类型谓词函数,instanceof左侧的值类型会按该类型谓词进行收窄,不满足类型谓词的内容无法访问将报错:
interface PointLike {
x: number;
y: number;
}
class Point implements PointLike {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x
this.y = y
}
distanceFromOrigin () {}
static [Symbol.hasInstance](val: unknown): val is PointLike {
return !!val && typeof val === 'object' &&
'x' in val && 'y' in val &&
typeof val.x === 'number' && typeof val.y === 'number'
}
}
function f(val: unknown) {
if (val instanceof Point) {
// 正常工作
val.x
val.y
// 5.3开始,将报错:
// Property 'distanceFromOrigin' does not exist on type 'PointLike'.
val.distanceFromOrigin()
}
}
在实例字段中检查super属性的访问
5.3版本开始,将会更仔细的检查在子类中通过super调用父类的方法,比如在子类中调用父类的类方法,将报错:
class Base {
// 注意此处是箭头函数,是类的属性,换成对象方法(普通函数)语法则不报错
someMethod = () => {
console.log('someMethod called')
}
}
class Derived extends Base {
someOtherMethod () {
super.someMethod()
}
}
// 报错:
// Class field 'someMethod' defined by the parent class is not accessible in the child class via super.
new Derived().someOtherMethod()
vscode插件/演练场点击类型支持跳转到类型定义的地方
TypeScript: Documentation - TypeScript 5.3 (typescriptlang.org)
类型自动导入的首选项设置
5.3版本开始,可以在vs code中通过extensions - typescript插件Typescript > Preference: Prefer Type Only Auto Imports,或者用户设置文件setting.json中添加typescript.preferences.preferTypeOnlyAutoImports为true开启。在开启之后,当自动导入一个类型时,会加上type:
// 2. 插件自动导入
// 开启该选项后:自动加type关键字
import { type Person } from './types'
// 未开启则是:
import { Person } from './types'
// 1. 编写代码:export let p: Person
跳过JSDoc解析进行优化
在通过tsc运行typescript时,编译器将避免解析JSDoc,以减少解析时间、减少存储注释的内存使用量、减少垃圾回收时间。
通过比较Non-Normalized交叉进行优化
对于A & (B1 | B2 | ... | B999999999),避免将其标准化为A & B1 | ... | A & B999999的形式
TypeScript: Documentation - TypeScript 5.3 (typescriptlang.org)
合并Typescript附带的两个库文件tsserverlibrary.js和typescript.js
合并Typescript附带的两个库文件tsserverlibrary.js和typescript.js为typescript.js
Typescript 5.2
using声明和显式资源管理
TypeScript: Documentation - TypeScript 5.2 (typescriptlang.org)
装饰器元数据
TypeScript: Documentation - TypeScript 5.2 (typescriptlang.org)
具名和匿名元组元素
在5.2版本之前,元组类型支持元素标签具名可选,但含有具名标签的元素不能和匿名标签的元素混用,即下面的将报错:
// 报错,其中first: T是具名标签,第二个元素是匿名标签
type Pair<T> = [first: T, T]
5.2版本中,取消了上述限制,同时在元组合并时,将保留之前的标签到新元组中,例如:
type HasLabels = [a: string, b: string]
type HasNoLabels = [number, number]
// 之前:type Merged = [number, number, string, string]
// 之后:type Merged = [number, number, a: string, b: string]
type Merged = [...HasNoLabels, ...HasLabels]
数组联合更简单的用法
在5.2版本中,数组的并集将从每个成员的类型构造出一个新的数组类型,比如string[] | number[]将会转为(string | number)[]。数组的方法(如filter、find、some、every、reduce)都应该在数组的并集上调用
在typescript实现的文件扩展下使用纯类型导入路径
5.2版本开始,不管是否启用allowImportingTsExtensions,都允许在.ts, .mts, .cts, .tsx中使用纯类型导入
import type { JustAType } from './justTypes.ts'
插件-对象成员逗号补全
给对象增加新属性时,如果之前的属性没有添加逗号,5.2版本开始会自动插入缺少的逗号
插件-内联变量重构
消除不必要的中间变量,例如:
function doSomeWord () {
const path = '.some_temp_file'
const file = fs.openSync(path, 'rw')
// 点击提示,会自动重构:
const file = fs.openSync('.some_temp_file', 'rw')
// ...
}
持续性类型兼容检查优化
TypeScript: Documentation - TypeScript 5.2 (typescriptlang.org)
Typescript 5.1
函数返回值返回undefined将更容易
在5.1版本中,若函数返回值类型为undefined,则可以不显式使用return:
// 之前的版本:
function f(): undefind { return }
function f(): undefind { return undefined }
// 5.1开始,可以这样,此处不会报错
function f(): undefined {}
其次,如果一个函数没有返回值,typescript也会将该函数的返回值类型推断为undefined。
getter和setter可以是不相关的两种类型
5.1版本之前,get的类型必须是set的类型的子类型。由于许多现有api和提议中getter和setter具有完全不相关的类型,故而在5.1版本中,可以通过显式的类型注释标注两种不同的类型,比如:
// 例子1:dom中的style属性
interface CSSStyleRule {
get style(): CSSStyleDeclaration;
set style(newValue: string);
}
// 例子2:
class SafeBox {
#value: string | undefined;
set value(newValue: string) {
// ...
}
// 和`--exactOptionalProperties`选项检查可选属性类似
get value(): string | undefined {
return this.#value
}
}
JSX元素和JSX标签之间的类型检查解耦
TypeScript: Documentation - TypeScript 5.1 (typescriptlang.org)
命名空间的JSX属性
import * as React from 'react'
// 下面两种形式等同
const x = <Foo a:b='hello' />
const y = <Foo a : b='hello' />
interface FooProps {
'a:b': string;
}
function Foo(props: FooProps) {
return <div>{props['a:b']}</div>
}
在模块解析中可参考typeRoots
当typescript指定的模块查找策略无法解析路径时,会将其解析成相对于typeRoots的路径
插件-移动声明到存在的文件中
TypeScript: Documentation - TypeScript 5.1 (typescriptlang.org)
插件-linked cursors for JSX tags
插件-JSDoc注释代码补全
Typescript 5.0
装饰器
新增内容:
originalMethod_context: 包括属性name、addInitializer等
装饰器不仅可以用在各种方法之上,还能够用在属性/字段、getter、setter和auto-accessors上,甚至是类本身也能够使用。
在旧版本中,若想启用装饰器,需要启用experimentalDecorators字段,但目前新增内容和该字段不兼容。
// 装饰器函数loggedMethod:改写原方法,添加一些信息比如log
// originalMethod: 原始方法,即下面的greet
// _context: 上下文对象,能够获取是否是私有/静态成员,方法名
function loggedMethod(originalMethod: any, _context: ClassMethodDecoratorContext) {
// 获取调用的方法名称
const methodName = String(context.name)
function replacementMethod(this: any, ...args: any[]) {
console.log(`log: entering method ${methodName}.`)
const result = originalMethod.call(this, ...args)
console.log(`log: exiting method ${methodName}.`)
return result
}
// 返回的方法替换了原始的方法定义(比如下面的greet调用)
return replacementMethod
}
// 装饰器函数bound:给原函数绑定this,避免this指向改变
// 由于没返回任何东西,所以不会修改原方法的内容
// 而是中初始化之前添加这段逻辑
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name
if (context.private) {
// 不能修饰私有属性
throw new Error(`bound cannot decorate private properties ${methodName as string}.`)
}
// addInitializer可以在构造函数中调用bind
context.addInitializer(function() {
this[methodName] = this[methodName].bind(this)
})
}
// 返回装饰器的函数loggedMethod2
function loggedMethod2(headMessage = 'log:') {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name)
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} entering method ${methodName}`)
const retult = originalMethod.call(this, ...args)
console.log(`${headMessage} exiting method ${methodName}.`)
return result
}
return replacementMethod
}
}
class Person {
name: string;
constructor(name: string) {
this.name = name
}
// 这里使用了装饰器后,调用greet时,将进入到装饰器函数中进行处理
// 装饰器的执行顺序,离方法最近的先执行,即:loggedMethod装饰greet, bound装饰loggedMethod的结果
@bound
@loggedMethod
greet(){
console.log(`hello, my name is ${this.name}.`)
}
// 也可以这样写:
@bound @loggedMethod greet() {}
// 调用一个返回装饰器函数的函数,效果和loggedMethod功能类似
@loggedMethod2('📄')
greet(){}
}
const p = new Person('ray')
p.greet()
// 输出:
// log: entering method.
// hello, my name is ray.
// log: exiting method.
// 在使用了bound装饰器之后,这样也不会丢失this
const greet = p.greet
greet()
装饰器也能单独用在export/export default之前或之后,但是不能同时在之前和之后一起使用:
@register export default class Foo {}
export default @register class Bar {}
// 报错:
@before export @after class Bar {}
若想编写一个类型友好的装饰器,必须使用This、Args、Return,分别对应于原始方法的this、参数、返回类型:
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>) {
const methodName = String(context.name)
function replacementMethod(this: This, ...args: Args): Return {
console.log(`log: entering method ${methodName}`)
const result = target.call(this, ...args)
console.log(`log: exiting method ${methodName}`)
return result
}
return replacementMethod
}
const类型参数
用于推断更加具体的类型,在旧版本中,通常用as const实现。
const修饰符不会拒绝可变值,也不会依赖不可变约束,使用可变类型约束可能会产生不一样的结果。在下面的例子中,readonly去掉之后,T的类型会被推断成string[](在5.3版本中,修复了该问题),因为只读数组不能用在可变数组中。
declare function fnGood<const T extends readonly string[]>(args: T): void;
// T: ['a', 'b', 'c']
fnGood(['a', 'b', 'c'])
const修饰符仅影响写在参数中的对象、数组、原始表达式的推断。如果参数中是一个变量,使用const不会产生任何效果:
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ['a', 'b', 'c']
// T:string[]
fnGood(arr)
extends字段支持多个配置文件
extends中的文件会共存,除非某些字段存在冲突,后面的会覆盖前面的相应字段:
{
"extends": ["a", "b", "c"],
"compilerOptions": {}
}
所有的枚举都是联合枚举
5.0版本中通过为每个计算成员创建一个唯一的类型,设法将所有的枚举转为一个联合枚举,意味着现在所有的枚举都可以进行类型收窄,并将成员当做类型引用。
--moduleResolution bundler
4.7版本引入了node16和nodenext选项用在--module和--moduleResolution字段上,该选项主要是用于在Node.js更精确查找es模块,但是该选项有很多约束,很多工具并未真正执行,例如Node.js的es模块中,相对导入必须写全文件扩展名。
大多数现代打包器都使用的es、commonJs融合的Node.js,新的选项策略--moduleResolution bundler就很适合:
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}
自定义标志解析
--allowImportingTsExtensions:允许ts文件使用特定的ts扩展名(.ts, .mts, .tsx)相互导入。仅当启用--noEmit或--emitDeclarationOnly后该标志才生效。
--resolvePackageJsonExports:如果是从node_modules的包中读取的会强制ts去查询package.json文件的exports字段。在node16、nodenext和moduleResolution: bundler中默认是true
--resolvePackageJsonImports:当从包含package.json的上级目录使用#开始搜索时,会强制查询package.json的import字段。在node16、nodenext和moduleResolution: bundler中默认是true
--allowArbitraryExtensions:当导入路径不是已经到js/ts文件扩展名时,编译器将根据{file basename}.d.{extension}.ts去查找对应的声明文件,默认情况下会报错。如果已经在运行时或者bundler中进行了相关处理,这时就可以用该选项消除错误。
--customConditions:当使用非require、node、import这种官方导出时,就需要使用该字段,比如customConditions: ["my-condition"],在package.json文件中,可以使用自定义的导出。仅在node16、nodenext和moduleResolution: bundler中有效:
{
"exports": {
".": {
"my-condition": "./foo.mjs",
"node": "./bar.mjs",
"require": "./baz.mjs"
}
}
}
--verbatimModuleSyntax
值导入导出将被保留,类型导出导出将被删除:
// 整个导入将被删除
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 *
// models/vehicles.ts
export class Spaceship {}
// models/index.ts
// 导出vehicles.ts中的所有类型,并具名为vehicles【注意是类型,不能做为值使用】
export tpye * as vehicles from './vehicles'
// main.ts
import { vehicles } from './models'
function takeASpaceship(s: vehicles.Spaceship) {
// 正常运行
}
JSDoc-@satisfies支持
JSDoc-@overload支持
在--build中传递emit-specific标志
为了更轻松的在不同的环境(生产、开发)中自定义某些内容,--build字段新增以下值用于判断:
--declaration--emitDeclarationOnly--declarationMap--sourceMap--inlineSourceMap
编辑器-自动化生成完整的switch/case
废弃的内容
5.0
最后一个支持的版本:5.4,继续使用需要指定选项:ignoreDeprecations: '5.0'
charsettarget: ES3importsNotUsedAsValuesnoImplicitUseStrictnoStrictGenericCheckskeyofStringsOnlysuppressExcessPropertyErrorssuppressImplicitAnyIndexErrorsoutpreserveValueImportsprependin project references- implicitly OS-specific
newLine
优化与变更
5.4
lib.d.ts的变更
为DOM生成的类型可能会影响代码库的类型检查
更精确的条件类型约束
5.4版本开始,以下代码将报错:
type IsArray<T> = T extends any[] ? true : false
function foo<U extends object>(x: IsArray<U>) {
// Type 'boolean' is not assignable to type 'true'.
let first: true = x
// Type 'boolean' is not assignable to type 'false'.
let second: false = x
}
更积极的减小类型变量和原始类型之交叉的结果
declare function intersect<T, U>(x; T, y: U): T & U;
function foo<T extends 'abc' | 'def'>(x: T, str: string, num: number) {
// 之前: T & string, 现在:T
let a = intersect(x, str)
// 之前:T & number, 现在:never
let b = intersect(x, num)
// 之前:(T & 'abc') | (T & 'def'),现在:T
let c = Math.random() < 0.5 ? intersect(x, 'abc') : intersect(x, 'def')
}
改进了带插值的模板字符串的检查
5.4版本开始,能够更准确的检查字符串是否可以分配给模板字符串类型的占位插槽:
function a<T extends { id: string }>() {
let x: `-${keyof T & string}`
// 此处不报错
x = 'id'
}
仅类型(Type-only)导入和当前模块值同名冲突时报错
5.4版本中,在isolatedModules开启时以下代码将报错:
import { Something } from './some/path'
let Something = 132
解决方法:
import type { Something } from './some/path'
// 或者是:
import { type Something } from './some/path'
新的枚举可分配限制
5.4版本开始,两个枚举兼容,必须保证枚举成员以及其对应的值也要相同,下面的例子将报错:
namespace First {
export enum SomeEnum {
A = 0,
B = 1
}
}
namespace Second {
export enum SomeEnum {
A = 0,
B = 2
}
}
function foo(x: First.SomeEnum, y: Second.SomeEnum) {
// Type 'Second.SomeEnum' is not assignable to type 'First.SomeEnum'. Each declaration of 'SomeEnum.B' differs in its value, where '1' was expected but '2' was given.
x = y
// Type 'First.SomeEnum' is not assignable to type 'Second.SomeEnum'. Each declaration of 'SomeEnum.B' differs in its value, where '2' was expected but '1' was given.
y = x
}
枚举成员名称限制
5.4版本开始,不再允许Infinity, -Infinity, NaN作为枚举成员的名称
在具有any类型的rest元素的元组上保留映射类型
Promise.all(['', ...([] as any)]).then(res => {
// 之前:any,现在:string
const head = res[0]
// 之前:any,现在:any[]
const tail = res.slice(1)
})
5.3
lib.d.ts的变更
为DOM生成的类型可能会影响代码库的类型检查
在实例属性中检查super的访问
5.2
lib.d.ts的变更
为DOM生成的类型可能会影响代码库的类型检查
labeledElementDeclarations可以是undefined元素
为了支持具名标签元素和匿名元素混合,对元组类型进行修改:
interface TupleType {
- labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];
+ labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[];
}
module和moduleResolution的值必须匹配最近的Node.js设置
在5.2版本中,如果这两个选项设置不匹配,则将报错。比如--module esnext --moduleResolution node16将会报错,最好使用--module nodenext或--module esnext --moduleResolution bundler
标记合并的一致性导出检查
5.2版本中,当两个声明合并时,如果是否全部导出未达成一致时,将报错:
declare module 'replace-in-file' {
// Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
export function replaceInFile(config: unknown)
export {}
// Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
namespace replaceInFile {
export function sync(config: unknown): unknown[];
}
}
5.1
最低版本要求ES2020和Node.js 14.17
显式禁用typeRoots向上搜寻node_modules/@types
5.1版本之前,当中tsconfig.json中对typeRoots的目录解析失败时,会继续向上搜寻父目录,并解析每个父目录的node_modules/@types。此行为在5.1版本中被禁用,解决方法是修改typeRoots字段的值,将正确的types所处的目录加上:
{
"compilerOptions": {
"types": ["node", "mocha"],
"typeRoots": [
"./some-custom-types/",
"./node_modules/@types",
"../../node_modules/@types"
]
}
}
5.0
运行时要求ES2018和Node.js10
lib.d.ts的变更
为DOM生成的类型可能会影响代码库的类型检查
关系运算符禁止隐式转换
5.0新增的有影响的关系运算符有:>, <, <=, >=
如果想要显式强制转为number,可以在相关变量之前加上+,然后进行关系比较,比如+sringVar > 4
枚举的变化
用--experimentalDecorators对构建函数的参数装饰器进行更准确的类型检查
5.0版本下面的内容会报错:
export declare const inject:
(entity: any) => (target: object, key: string | symbol, index?: number) => void;
export class Foo {}
export class C {
// 报错:Argument of type 'undefined' is not assignable to parameter of type 'string | symbol'.
constructor(@inject(Foo) private x: any) {}
}