宣布推出 TypeScript 4.1
今天我们很荣幸发布 TypeScript 4.1!
如果您还不熟悉 TypeScript,它是一种基于 JavaScript 的语言,通过添加类型声明和注解语法来实现。TypeScript 编译器可以使用这些语法对我们的代码进行类型检查,然后输出简洁易读的 JavaScript 代码,这些代码可以在多种不同的运行时环境中运行。静态类型检查可以在我们运行代码之前,甚至在保存文件之前就告知我们代码中的错误,这得益于 TypeScript 在各种编辑器中强大的编辑功能。除了错误检查之外,TypeScript 还为一些您常用的编辑器提供了代码补全、快速修复和重构等功能,这些功能同时支持 TypeScript和JavaScript。事实上,如果您已经在使用 Visual Studio 或 Visual Studio Code,那么在编写 JavaScript 代码时,您可能已经在使用 TypeScript 了!
如果您想了解更多信息,请访问我们的网站!
但如果你的项目中已经使用了 TypeScript,你可以通过 NuGet获取它,或者使用 npm 并运行以下命令:
npm install -D typescript
您还可以通过以下方式获得编辑支持
本次版本更新带来了一些令人兴奋的新功能、新的检查标志、编辑器效率提升以及速度改进。让我们一起来看看 4.1 版本都带来了哪些新内容!
- 模板字面类型
- 映射类型中的键重映射
- 递归条件类型
--noUncheckedIndexedAccesspaths没有baseUrlcheckJs暗示allowJs- React 17 JSX 工厂
- 编辑器支持 JSDoc
@see标签 - 重大变更
模板字面类型
TypeScript 中的字符串字面量类型允许我们对需要一组特定字符串的函数和 API 进行建模。
function setVerticalAlignment(pos: "top" | "middle" | "bottom") {
// ...
}
setVerticalAlignment("middel");
// ~~~~~~~~
// error: Argument of type '"middel"' is not assignable to
// parameter of type '"top" | "middle" | "bottom"'.
这非常好,因为字符串字面量类型基本上可以对我们的字符串值进行拼写检查。
我们也很喜欢字符串字面量可以用作映射类型中的属性名。从这个意义上讲,它们也可以用作构建块。
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };
但是,字符串字面量类型还可以用作构建模块:构建其他字符串字面量类型。
这就是 TypeScript 4.1 引入模板字面量字符串类型的原因。它的语法与JavaScript 中的模板字面量字符串相同,但用于类型位置。当与具体的字面量类型一起使用时,它会通过连接内容生成一个新的字符串字面量类型。
type World = "world";
type Greeting = `hello ${World}`;
// same as
// type Greeting = "hello world";
当联合体位于替换位置时会发生什么?
它会生成一个集合,其中包含每个联合体成员可以表示的所有可能的字符串字面量。
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";
除了在发布说明中提供一些有趣的示例之外,这种方法还有更多用途。例如,许多 UI 组件库的 API 都提供了同时指定垂直和水平对齐方式的功能,通常使用类似 `::align-overflow: 0` 这样的字符串来同时设置两者"bottom-right"。垂直对齐方式有"top"`:: align-overflow: 0` "middle"、` ::align-overflow: 0` 和 `::align-overflow: 0`,水平对齐方式有 `::align-overflow: 0`、`::align-overflow: 0`和 `:: align -overflow: 0`,总共有 9 种可能的字符串,其中每个垂直对齐方式的字符串都用短横线 (-) 连接,每个水平对齐方式的字符串都用短横线连接"bottom"。"left""center""right"
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
// Takes
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle
虽然这类 API 的例子在实际应用中很多,但这仍然只是一个简单的示例,因为我们可以手动编写这些代码。实际上,对于 9 个字符串来说,这可能没问题;但是,当需要大量字符串时,您应该考虑预先自动生成它们,以节省每次类型检查的工作量(或者直接使用 `String` ,string这样更容易理解)。
动态创建新的字符串字面量是其真正价值的一部分。例如,设想一个makeWatchedObjectAPI,它接收一个对象,并生成一个几乎完全相同的对象,但添加了一个新on方法来检测属性的变化。
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});
请注意,它on监听的是事件"firstNameChanged",而不仅仅是"firstName"。我们应该如何编写这段代码?
type PropEventSource<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
这样,我们就可以构建一个当提供错误属性时会报错的程序了!
// error!
person.on("firstName", () => {
});
// error!
person.on("frstNameChanged", () => {
});
我们还可以在模板字面量类型中进行一些特殊操作:我们可以根据替换位置进行推断。我们可以将最后一个例子泛化,使其能够根据eventName字符串的部分内容推断出关联的属性。
type PropEventSource<T> = {
on<K extends string & keyof T>
(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
firstName: "Homer",
age: 42,
location: "Springfield",
});
// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
// 'newName' has the type of 'firstName'
console.log(`new name is ${newName.toUpperCase()}`);
});
// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
if (newAge < 0) {
console.log("warning! negative age");
}
})
这里我们将其变成on了一个通用方法。当用户使用字符串调用该方法时"firstNameChanged',TypeScript 会尝试推断出正确的类型K。为此,它会匹配字符串K之前的内容"Changed"并推断出字符串"firstName"。一旦 TypeScript 确定了类型,该方法就可以获取原始对象上的on类型,在本例中为。类似地,当我们使用时调用,它会找到属性的类型,即。firstNamestring"ageChanged"agenumber
类型推断可以以不同的方式组合使用,通常用于解构字符串,并以不同的方式重构它们。事实上,为了帮助修改这些字符串字面量类型,我们添加了一些新的实用类型别名,用于修改字母的大小写(即转换为小写和大写字符)。
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
type HELLO = EnthusiasticGreeting<"hello">;
// same as
// type HELLO = "HELLO";
新的类型别名有Uppercase`\ Lowercaset`、`\ Capitalizecdot` 和Uncapitalize`\cdot`。前两个别名转换字符串中的每个字符,后两个别名仅转换字符串中的第一个字符。
有关更多详细信息,请参阅原始拉取请求和正在进行的切换到类型别名助手的拉取请求。
映射类型中的键重映射
简单回顾一下,映射类型可以基于任意键创建新的对象类型。
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };
或者基于其他对象类型的新对象类型。
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
[K in keyof T]?: T[K]
};
到目前为止,映射类型只能生成具有您提供的键的新对象类型;但是,很多时候您希望能够根据输入创建新键或过滤键。
这就是为什么 TypeScript 4.1 允许你使用新的子句重新映射映射类型中的键as。
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}
有了这个新as条款,您可以利用模板字面量类型等功能,轻松地根据旧属性名称创建属性名称。
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
你甚至可以通过生成来过滤掉某些键never。这意味着在某些情况下,你不需要使用额外的Omit辅助类型。
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// same as
// type KindlessCircle = {
// radius: number;
// };
有关更多信息,请查看GitHub 上的原始拉取请求。
递归条件类型
在 JavaScript 中,我们经常会看到一些函数可以对容器类型进行任意层级的扁平化和构建。例如,考虑 ` .then()Object` 实例上的 ` Promise.`方法,.then(...)它会解包每个 Promise,直到找到一个“非 Promise 类型”的值,并将该值传递给回调函数。此外,`Object` 还有一个相对较新的flat方法Array,可以接受一个深度参数来指定扁平化的深度。
在 TypeScript 的类型系统中,实际上无法实现这一点。虽然有一些变通方法可以做到这一点,但最终得到的类型看起来非常不合理。
这就是为什么 TypeScript 4.1 放宽了对条件类型的一些限制——以便它们能够更好地模拟这些模式。在 TypeScript 4.1 中,条件类型现在可以在其分支内直接引用自身,从而更容易编写递归类型别名。
例如,如果我们想编写一个类型来获取嵌套数组的元素类型,我们可以编写以下deepFlatten类型。
type ElementType<T> =
T extends ReadonlyArray<infer U> ? ElementType<U> : T;
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
throw "not implemented";
}
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);
类似地,在 TypeScript 4.1 中,我们可以编写一个Awaited类型来深度解包Promises。
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
p: Promise<T>,
onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;
请记住,虽然这些递归类型功能强大,但应该负责任地、谨慎地使用它们。
首先,这些类型可以完成大量工作,这意味着它们会增加类型检查的时间。尝试对考拉兹猜想或斐波那契数列中的数字进行建模可能很有趣,但不要将这些内容放在.d.tsnpm 上的文件中。
但除了计算量大之外,这些类型在处理足够复杂的输入时可能会达到内部递归深度限制。一旦达到递归深度限制,就会导致编译时错误。一般来说,与其编写在更实际的示例中会出错的代码,不如完全避免使用这些类型。
更多信息请参见实现部分。
已检查索引访问(--noUncheckedIndexedAccess)
TypeScript has a feature called index signatures. These signatures are a way to signal to the type system that users can access arbitrarily-named properties.
interface Options {
path: string;
permissions: number;
// Extra properties are caught by this index signature.
[propName: string]: string | number;
}
function checkOptions(opts: Options) {
opts.path // string
opts.permissions // number
// These are all allowed too!
// They have the type 'string | number'.
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}
In the above example, Options has an index signature that says any accessed property that's not already listed should have the type string | number. This is often convenient for optimistic code that assumes you know what you're doing, but the truth is that most values in JavaScript do not support every potential property name. Most types will not, for example, have a value for a property key created by Math.random() like in the previous example. For many users, this behavior was undesirable, and felt like it wasn't leveraging the full strict-checking of --strictNullChecks.
That's why TypeScript 4.1 ships with a new flag called --noUncheckedIndexedAccess. Under this new mode, every property access (like foo.bar) or indexed access (like foo["bar"]) is considered potentially undefined. That means that in our last example, opts.yadda will have the type string | number | undefined as opposed to just string | number. If you need to access that property, you'll either have to check for its existence first or use a non-null assertion operator (the postfix ! character).
// Checking if it's really there first.
if (opts.yadda) {
console.log(opts.yadda.toString());
}
// Basically saying "trust me I know what I'm doing"
// with the '!' non-null assertion operator.
opts.yadda!.toString();
One consequence of using --noUncheckedIndexedAccess is that indexing into an array is also more strictly checked, even in a bounds-checked loop.
function screamLines(strs: string[]) {
// this will have issues
for (let i = 0; i < strs.length; i++) {
console.log(strs[i].toUpperCase());
// ~~~~~~~
// error! Object is possibly 'undefined'.
}
}
If you don't need the indexes, you can iterate over individual elements by using a for-of loop or a forEach call.
function screamLines(strs: string[]) {
// this works fine
for (const str of strs) {
console.log(str.toUpperCase());
}
// this works fine
strs.forEach(str => {
console.log(str.toUpperCase());
});
}
This flag can be handy for catching out-of-bounds errors, but it might be noisy for a lot of code, so it is not automatically enabled by the --strict flag; however, if this feature is interesting to you, you should feel free to try it and determine whether it makes sense for your team's codebase!
You can learn more at the implementing pull request.
paths without baseUrl
Using path-mapping is fairly common - often it's to have nicer imports, often it's to simulate monorepo linking behavior.
Unfortunately, specifying paths to enable path-mapping required also specifying an option called baseUrl, which allows bare specifier paths to be reached relative to the baseUrl too. This also often caused poor paths to be used by auto-imports.
In TypeScript 4.1, the paths option can be used without baseUrl. This helps avoid some of these issues.
checkJs Implies allowJs
Previously if you were starting a checked JavaScript project, you had to set both allowJs and checkJs. This was a slightly annoying bit of friction in the experience, so checkJs now implies allowJs by default.
See more details at the pull request.
React 17 JSX Factories
TypeScript 4.1 supports React 17's upcoming jsx and jsxs factory functions through two new options for the jsx compiler option:
react-jsxreact-jsxdev
These options are intended for production and development compiles respectively. Often, the options from one can extend from the other. For example, a tsconfig.json for production builds might look like the following:
// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}
and one for development builds might look like the following:
// ./src/tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}
For more information, check out the corresponding PR.
Editor Support for the JSDoc @see Tag
The JSDoc tag @see tag now has better support in editors for TypeScript and JavaScript. This allows you to use functionality like go-to-definition in a dotted name following the tag. For example, going to definition on first or C in the JSDoc comment just works in the following example:
// @filename: first.ts
export class C { }
// @filename: main.ts
import * as first from './first';
/**
* @see first.C
*/
function related() { }
Thanks to frequent contributor Wenlu Wang for implementing this!
Breaking Changes
lib.d.ts Changes
lib.d.ts may have a set of changed APIs, potentially in part due to how the DOM types are automatically generated. One specific change is that Reflect.enumerate has been removed, as it was removed from ES2016.
abstract Members Can't Be Marked async
Members marked as abstract can no longer be marked as async. The fix here is to remove the async keyword, since callers are only concerned with the return type.
any/unknown Are Propagated in Falsy Positions
Previously, for an expression like foo && somethingElse, the type of foo was any or unknown, the type of the whole that expression would be the type of somethingElse.
For example, previously the type for x here was { someProp: string }.
declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;
However, in TypeScript 4.1, we are more careful about how we determine this type. Since nothing is known about the type on the left side of the &&, we propagate any and unknown outward instead of the type on the right side.
The most common pattern we saw of this tended to be when checking compatibility with booleans, especially in predicate functions.
function isThing(x: any): boolean {
return x && typeof x === 'object' && x.blah === 'foo';
}
Often the appropriate fix is to switch from foo && someExpression to !!foo && someExpression.
resolve's Parameters Are No Longer Optional in Promises
When writing code like the following
new Promise(resolve => {
doSomethingAsync(() => {
doSomething();
resolve();
})
})
You may get an error like the following:
resolve()
error TS2554: Expected 1 arguments, but got 0.
An argument for 'value' was not provided.
This is because `resolve` no longer has an optional parameter, so by default, it must now be passed a value. Often this catches legitimate bugs with using `Promise`s. The typical fix is to pass it the correct argument, and sometimes to add an explicit type argument.
```ts
new Promise<number>(resolve => {
// ^^^^^^^^
doSomethingAsync(value => {
doSomething();
resolve(value);
// ^^^^^
})
})
However, sometimes resolve() really does need to be called without an argument. In these cases, we can give Promise an explicit void generic type argument (i.e. write it out as Promise<void>). This leverages new functionality in TypeScript 4.1 where a potentially-void trailing parameter can become optional.
new Promise<void>(resolve => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
})
})
TypeScript 4.1 ships with a quick fix to help fix this break.
Conditional Spreads Create Optional Properties
In JavaScript, object spreads (like { ...foo }) don't operate over falsy values. So in code like { ...foo }, foo will be skipped over if it's null or undefined.
Many users take advantage of this to spread in properties "conditionally".
interface Person {
name: string;
age: number;
location: string;
}
interface Animal {
name: string;
owner: Person;
}
function copyOwner(pet?: Animal) {
return {
...(pet && pet.owner),
otherStuff: 123
}
}
// We could also use optional chaining here:
function copyOwner(pet?: Animal) {
return {
...(pet?.owner),
otherStuff: 123
}
}
Here, if pet is defined, the properties of pet.owner will be spread in - otherwise, no properties will be spread into the returned object.
The return type of copyOwner was previously a union type based on each spread:
{ x: number } | { x: number, name: string, age: number, location: string }
This modeled exactly how the operation would occur: if pet was defined, all the properties from Person would be present; otherwise, none of them would be defined on the result. It was an all-or-nothing operation.
However, we've seen this pattern taken to the extreme, with hundreds of spreads in a single object, each spread potentially adding in hundreds or thousands of properties. It turns out that for various reasons, this ends up being extremely expensive, and usually for not much benefit.
In TypeScript 4.1, the returned type sometimes uses all-optional properties.
{
x: number;
name?: string;
age?: number;
location?: string;
}
This ends up performing better and generally displaying better too.
For more details, see the original change. While this behavior is not entirely consistent right now, we expect a future release will produce cleaner and more predictable results.
Unmatched parameters are no longer related
TypeScript would previously relate parameters that didn't correspond to each other by relating them to the type any. With changes in TypeScript 4.1, the language now skips this process entirely. This means that some cases of assignability will now fail, but it also means that some cases of overload resolution can fail as well. For example, overload resolution on util.promisify in Node.js may select a different overload in TypeScript 4.1, sometimes causing new or different errors downstream.
As a workaround, you may be best using a type assertion to squelch errors.
What's Next?
We hope that TypeScript 4.1 makes coding feel perfectly splendid. To stay in the loop on our next version, you can track the 4.2 Iteration Plan and our Feature Roadmap as it comes together.
Happy Hacking!
- Daniel Rosenwasser and the TypeScript Team
文章来源:https://dev.to/typescript/announcing-typescript-4-1-1ld5