Added Awaited type
Awaited can extract the actual return type of Promise. According to the name, it can be understood as: waiting for the type obtained after the Promise resolves. The following is the Demo provided by the official documentation:
// A = string
type A = Awaited<Promise<string>>;
// B = number
type B = Awaited<Promise<Promise<number>>>;
// C = boolean | number
type C = Awaited<boolean | Promise<number>>;
Bundled dom lib types can be replaced
Because of its out-of-the-box features, TS bundles all dom built-in types. For example, we can directly use the Document type, which is provided by TS built-in.
Maybe sometimes you don't want to upgrade the associated dom built-in type with the TS version upgrade, so TS provides a solution to specify the dom lib type, which can be declared in package.json
@typescript/lib-dom
:
{
"dependencies": {
"@typescript/lib-dom": "npm:@types/web"
}
}
This feature improves the environmental compatibility of TS, but in general, it is recommended to use it out of the box, eliminating the need for tedious configuration and better maintenance of the project.
Template string types also support type narrowing
export interface Success {
type: `${string}Success`;
body: string;
}
export interface Error {
type: `${string}Error`;
message: string;
}
export function handler(r: Success | Error) {
if (r.type === "HttpSuccess") {
// 'r' has type 'Success'
let token = r.body;
}
}
The template string type has been supported for a long time, but now it is supported to narrow the type according to the template string in the branch condition.
Add new --module es2022
While you can use --module esnext to keep up to date with features, if you want to use stable version numbers and support top-level await features, you can use es2022.
tail recursion optimization
The TS type system supports tail recursion optimization. Take the following example to understand:
type TrimLeft<T extends string> =
T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
// error: Type instantiation is excessively deep and possibly infinite.
type Test = TrimLeft<" oops">;
Before tail recursion optimization, TS will report an error because the stack is too deep, but now it can return the execution result correctly, because after tail recursion optimization, it will not form a gradually deepening call, but exit the current function immediately after execution, The number of stacks always remains the same.
JS has not yet achieved automatic tail recursion optimization, but it can be simulated through a custom function TCO. The implementation of this function is given below:
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator(...rest) {
accumulated.push(rest);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
The core is to turn recursion into a while loop, so that no stack is generated.
Force retention of imports
TS will kill the unused imports when compiling, but this time the --preserveValueImports
parameter is provided to disable this feature, because the following conditions will cause the import to be removed by mistake:
import { Animal } from "./animal.js";
eval("console.log(new Animal().isDangerous())");
Because TS can't distinguish references in eval, similar to vue's setup
syntax:
<!-- A .vue File -->
<script setup>
import { someFunc } from "./some-module.js";
</script>
<button @click="someFunc">Click me!</button>
Support variable import type declaration
Variables referenced by the following syntax tags were previously supported as types:
import type { BaseType } from "./some-module.js";
Variable-level type declarations are now supported:
import { someFunc, type BaseType } from "./some-module.js";
This makes it easy to safely erase BaseType
when building an independent module, because when a single module is built, it cannot perceive the content of the some-module.js
file, so if it is not specified type BaseType
, the TS compiler will not recognize it as a type variable.
class private variable check
Contains two features, the first is that TS supports the inspection of class private variables:
class Person {
#name: string;
}
The second is to support the judgment of #name in obj
, such as:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
equals(other: unknown) {
return other &&
typeof other === "object" &&
#name in other && // <- this is new!
this.#name === other.#name;
}
}
This judgment implicitly requires that #name in other
of other
is an object instantiated by Person, because this syntax can only exist in a class, and it can be further narrowed down to the Persion class.
Import Assertion
The import assertion proposal is supported:
import obj from "./something.json" assert { type: "json" };
and assertions for dynamic imports:
const obj = await import("./something.json", {
assert: { type: "json" }
})
TS This feature supports any type of assertion, regardless of whether the browser recognizes it. So if the assertion is to take effect, it needs either of the following two supports:
- Browser support.
- Build script support.
However, at present, the syntax supported by build scripts is not uniform. For example, Vite asserts the import type in the following two ways:
import obj from "./something?raw"
// 或者自创的语法 blob 加载模式
const modules = import.meta.glob(
'./**/index.tsx',
{
assert: { type: 'raw' },
},
);
Therefore, the import assertion can at least unify the syntax of the build tool in the future, and even after the browser supports it natively, there is no need for the build tool to process the import assertion.
In fact, there is still a long way to go completely relying on browser parsing, because a complex front-end project has at least 3000~5000 resource files, and it is impossible to use bundles to load these resources one by one in the production environment, because the speed is too slow.
const read-only assertion
const obj = {
a: 1
} as const
obj.a = 2 // error
All attributes of the object specified by this syntax are readonly
.
Faster loading with realpathSync.native
I don't know much about developers, just use realpathSync.native
to improve the TS loading speed.
Fragment auto-completion enhancements
The automatic completion function of Class member functions and JSX properties has been enhanced. After using the latest version of TS, you should already have a sense of movement. For example, after entering a carriage return in a JSX writing tag, it will automatically complete the content according to the type, such as:
<App cla />
// ↑回车↓
// <App className="|" />
// ↑光标自动移到这里
Code can be written before super()
The restriction of JS on super()
is that this cannot be called before, but the TS restriction is more strict. Any code written before super()
will report an error, which is obviously too strict.
Now TS has relaxed the verification policy, only calling this before super()
will report an error, and executing other codes is allowed.
In fact, this should have been changed a long time ago. Such a strict verification policy made me think that JS would not allow any function to be called before super()
, but I thought it was unreasonable, because super()
that the constructor
function of the parent class is called. The reason why it is not called automatically, but needs to be called manually super()
is that developers can flexibly decide which logic is executed before the constructor of the parent class, so The one-size-fits-all behavior of TS actually caused super()
to lose its meaning and become a meaningless template code.
Type narrowing also works for destructuring
This feature is really powerful, that is, the type narrowing still takes effect after destructuring.
Previously, the narrowing of TS types was already very powerful, and the following judgments could be made:
function foo(bar: Bar) {
if (bar.a === '1') {
bar.b // string 类型
} else {
bar.b // number 类型
}
}
But if a and b are deconstructed from bar in advance, they cannot be automatically narrowed. Now the problem has also been solved, and the following code can also work normally:
function foo(bar: Bar) {
const { a, b } = bar
if (a === '1') {
b // string 类型
} else {
b // number 类型
}
}
Deep recursive type checking optimization
The following assignment statement produces an exception because the type of the property prop does not match:
interface Source {
prop: string;
}
interface Target {
prop: number;
}
function check(source: Source, target: Target) {
target = source;
// error!
// Type 'Source' is not assignable to type 'Target'.
// Types of property 'prop' are incompatible.
// Type 'string' is not assignable to type 'number'.
}
This is easy to understand. From the point of view of the error report, TS will also find the prop type mismatch according to the recursive detection method. But since TS supports generics, the following is an example of infinite recursion:
interface Source<T> {
prop: Source<Source<T>>;
}
interface Target<T> {
prop: Target<Target<T>>;
}
function check(source: Source<string>, target: Target<number>) {
target = source;
}
In fact, it doesn't need to be as complicated as the official description says, even props: Source<T>
is enough to make the example recurse infinitely. In order to ensure that there is no error in this situation, TS makes a recursion depth judgment. Too deep recursion will terminate the judgment, but this will bring a problem, that is, the following errors cannot be recognized:
interface Foo<T> {
prop: T;
}
declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;
x = y;
In order to solve this problem, TS made a judgment: recursive protection only takes effect in the scenario of recursive writing, and the above example, although it is also a deep recursion, but because it is written by a person, TS will also take the trouble. Recursively go down one by one, so the scene can work correctly.
The core of this optimization is that TS can analyze the recursion caused by "very abstract/heuristic" writing according to the code structure, which is the recursion caused by enumeration one by one, and exempt the latter from the recursion depth check.
Enhanced index deduction
The examples given in the official documents below look complicated at first glance, so let's disassemble and analyze them:
interface TypeMap {
"number": number;
"string": string;
"boolean": boolean;
}
type UnionRecord<P extends keyof TypeMap> = { [K in P]:
{
kind: K;
v: TypeMap[K];
f: (p: TypeMap[K]) => void;
}
}[P];
function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
record.f(record.v);
}
// This call used to have issues - now works!
processRecord({
kind: "string",
v: "hello!",
// 'val' used to implicitly have the type 'string | number | boolean',
// but now is correctly inferred to just 'string'.
f: val => {
console.log(val.toUpperCase());
}
})
The purpose of this example is to achieve processRecord
function, which pass parameters by identifying kind
automatically derive callback f
in value
of type.
For example kind: "string"
, then val
is a string type, kind: "number"
, then val
.
Because this update of TS solves the problem of the type of val
that could not be recognized before, we don't need to care how TS solves it, just remember that TS can correctly identify the scene (a bit like the formula of Go, for the classic example It's best to learn one by one), and understand how the scene is constructed.
How to do it? First define a typemap:
interface TypeMap {
"number": number;
"string": string;
"boolean": boolean;
}
Then define the final function processRecord
:
function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
record.f(record.v);
}
A generic K is defined here, K extends keyof TypeMap
is equivalent to K extends 'number' | 'string' | 'boolean'
, so here is the value range of the following generic K, which is one of the three strings.
The point is that the parameter record
needs to be determined according to the incoming kind
f
callback function parameter type. Let's first imagine how to write the following UnionRecord
type:
type UnionRecord<K extends keyof TypeMap> = {
kind: K;
v: TypeMap[K];
f: (p: TypeMap[K]) => void;
}
As above, the natural idea is to define a generic K, so kind
and f
, p
types can be represented, so processRecord<K extends keyof TypeMap>(record: UnionRecord<K>)
The UnionRecord<K>
means that the current received actual type K is passed in UnionRecord
, so that UnionRecord
knows what type is actually processed.
This function has ended when I came here, but the official definition UnionRecord
is slightly different:
type UnionRecord<P extends keyof TypeMap> = { [K in P]:
{
kind: K;
v: TypeMap[K];
f: (p: TypeMap[K]) => void;
}
}[P];
This example has deliberately increased the complexity, and used the index method to go around it. Maybe TS could not parse this form before. In short, this writing method is now supported. Let's see why this writing is equivalent to the above. The above writing is simplified as follows:
type UnionRecord<P extends keyof TypeMap> = {
[K in P]: X
}[P];
It can be interpreted as, UnionRecord
defines a generic type P, and the function obtains the type from the object { [K in P]: X }
according to the index (or understood as a subscript) [P]
. And [K in P]
This type definition describing the Key value of an object is equivalent to defining multiple types. Since it happens to be P extends keyof TypeMap
, you can understand that the type is expanded like this:
type UnionRecord<P extends keyof TypeMap> = {
'number': X,
'string': X,
'boolean': X
}[P];
And P is a generic type. Because of the definition of [K in P]
, it must be able to hit one of the above, so it is actually equivalent to the following simple writing:
type UnionRecord<K extends keyof TypeMap> = {
kind: K;
v: TypeMap[K];
f: (p: TypeMap[K]) => void;
}
Parametric Control Flow Analysis
The literal translation of this feature is quite strange, let’s understand it from the code:
type Func = (...args: ["a", number] | ["b", string]) => void;
const f1: Func = (kind, payload) => {
if (kind === "a") {
payload.toFixed(); // 'payload' narrowed to 'number'
}
if (kind === "b") {
payload.toUpperCase(); // 'payload' narrowed to 'string'
}
};
f1("a", 42);
f1("b", "hello");
If you define parameters as arrays and use or concatenate enumerations, you potentially include a runtime type narrowing. For example, when the value of the first parameter is a
, the type of the second parameter is determined to be number
, and the value of the first parameter is b
. The parameter type is determined as string
.
It is worth noting that this type of type deduction is from front to back, because the parameters are passed from left to right, so the front is deduced from the front, and the front cannot be derived from the back (for example, it cannot be understood that the second parameter is number
type, the value of the first parameter must be a
).
Remove unnecessary code generated by JSX compilation
JSX compiles the last meaningless void 0
, reducing the code size:
- export const el = _jsx("div", { children: "foo" }, void 0);
+ export const el = _jsx("div", { children: "foo" });
Since the changes are small, you can take the opportunity to learn how to modify the TS source code. This is the PR DIFF address .
It can be seen that the modification location is the src/compiler/transformers/jsx.ts
file, and the change logic is to remove the factory.createVoidZero()
function, which, as its name, will create the end void 0
, except In addition, there are a large number of tests file modifications. In fact, it is not difficult to understand the source code context.
JSDoc validation prompt
Since JSDoc comments are separated from the code, it is easy to fork with the actual code as you continue to iterate:
/**
* @param x {number} The first operand
* @param y {number} The second operand
*/
function add(a, b) {
return a + b;
}
TS can now give hints for inconsistencies in naming, types, etc. By the way, try not to use JSDoc when TS is used, after all, there is a risk of inconsistency between code and type separation at any time.
Summarize
Judging from these two updates, TS has entered a mature stage, but TS is still in the early stage on the issue of generic classes. There are a large number of complex scenarios that cannot be supported, or there is no elegant compatible solution. I hope that it can continue to improve and complex in the future. The type of scene supported.
The discussion address is: Intensive Reading "New Features of Typescript 4.5-4.6" Issue #408 dt-fe/weekly
If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.
Follow Front-end Intensive Reading WeChat Official Account
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。