在JavaScript中,使用期望属性名称作为参数的API是相当普遍的,但到目前为止,还无法表达这些API中出现的类型关系。
输入索引类型查询或keyof
;索引类型查询keyof T
可以为T生成允许的属性名称类型。keyof T
类型被认为是一种string的子类型。
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
其中的双重属性是索引访问类型,也称为lookup类型。从语法上讲,它们看起来完全像元素访问,但是写成类型:
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string
您可以将此模式与类型系统的其他部分一起使用,以获得类型安全的查找。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // Inferred type is T[K]
}
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
let x = { foo: 10, bar: "hello!" };
let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string
let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"
setProperty(x, "foo", "string"); // Error!, string expected number
一个常见的任务是采用现有类型并使其每个属性完全可选。假设我们有一个Person:
interface Person {
name: string;
age: number;
location: string;
}
它的部分版本是:
interface PartialPerson {
name?: string;
age?: number;
location?: string;
}
使用Mapped类型,PartialPerson可以写为Person类型的广义转换,例如:
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PartialPerson = Partial<Person>;
映射类型是通过获取文字类型的并集,并为新对象类型计算一组属性来生成的。它们与Python中的列表推导类似,但它们不是在列表中生成新元素,而是在类型中生成新属性。
除此了Partial之外,Mapped Types还可以在类型上表达许多有用的转换:
// Keep types the same, but make each property to be read-only.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Same property names, but make the value a promise instead of a concrete one
type Deferred<T> = {
[P in keyof T]: Promise<T[P]>;
};
// Wrap proxies around properties of T
type Proxify<T> = {
[P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};
Partial和Readonly,如前所述,是非常有用的结构。您可以使用它们来描述一些常见的JS例程,例如:
function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;
因此,它们现在默认包含在标准库中。
我们还包括另外两种实用程序类型:Record和Pick。
// From T pick a set of properties K
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>
const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
TypeScript 2.1支持ESnext Spread和Rest。
与数组扩展类似,扩展对象可以很方便地获得浅层副本:
let copy = { ...original };
同样,您可以合并多个不同的对象。在以下示例中,merged将具有来自foo,bar和baz的属性。
let merged = { ...foo, ...bar, ...baz };
您还可以覆盖现有属性并添加新属性:
let obj = { x: 1, y: "string" };
var newObj = {...obj, z: 3, y: 4}; // { x: number, y: number, z: number }
指定扩展操作的顺序决定了生成的对象中最终的属性;以后的属性会在以前创建的属性上“win out”。
对象休息是对象扩展的双重对象,因为它们可以提取在解构元素时不会被拾取的任何额外属性:
let obj = { x: 1, y: 1, z: 1 };
let { z, ...obj1 } = obj;
obj1; // {x: number, y:number};
在TypeScript 2.1之前支持此功能,但仅在定位ES6/ES2015时。TypeScript 2.1为ES3和ES5运行时提供了功能,这意味着无论您使用何种环境,您都可以自由地利用它。
注意:首先,我们需要确保我们的运行时具有全局可用的ECMAScript兼容性Promise。这可能涉及为Promise获取一个polyfill,或者依赖一个你可能在你所定位的运行时间。我们还需要确保TypeScript知道Promise是存在的,通过将lib标志设置为类似于"dom", "es2015"或"dom", "es2015.promise", "es5"的东西。
{
"compilerOptions": {
"lib": ["dom", "es2015.promise", "es5"]
}
}
function delay(milliseconds: number) {
return new Promise<void>(resolve => {
setTimeout(resolve, milliseconds);
});
}
async function dramaticWelcome() {
console.log("Hello");
for (let i = 0; i < 3; i++) {
await delay(500);
console.log(".");
}
console.log("World!");
}
dramaticWelcome();
编译和运行输出应该会导致ES3/ES5引擎上的正确行为。
TypeScript注入了一些辅助函数,例如用于继承的__extends,在对象文字中用于扩展操作的__assign和JSX元素以及用于异步函数的__awaiter。
以前有两种选择:
这两个选项还有待改进;将帮助器捆绑在每个文件中对于试图保持其包装尺寸较小的客户来说是一个痛点。并且不包括帮助器,意味着客户必须维护自己的帮助程序库。
TypeScript 2.1允许在项目中将这些文件包含在一个单独的模块中,编译器将根据需要向它们发出导入。
首先,安装tslib实用程序库:
npm install tslib
其次,使用--importHelpers命令编译文件:
tsc --module commonjs --importHelpers a.ts
因此,给定以下输入,生成的.js文件将包含导入到tslib并使用其中的__assign帮助程序而不是内联它。
export const o = { a: 1, name: "o" };
export const copy = { ...o };
"use strict";
var tslib_1 = require("tslib");
exports.o = { a: 1, name: "o" };
exports.copy = tslib_1.__assign({}, exports.o);
传统上,TypeScript对于如何导入模块过于严格。这是为了避免拼写错误并阻止用户错误地使用模块。
但是,在很多时候,您可能只想导入可能没有自含.d.ts文件的现有模块。以前这是一个错误。从TypeScript 2.1开始,这现在变得更加容易。
使用TypeScript 2.1,您可以导入JavaScript模块而无需类型声明。如果存在类型声明(例如,declare module "foo" { ... },或node_modules/@types/foo)仍然具有优先权。
对没有声明文件的模块的导入仍将被标记为--noImplicitAny下的错误。
// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";
TypeScript 2.1支持三个新的目标值--target ES2016,--target ES2017和--target ESNext。
使用target --target ES2016将指示编译器不要转换特定于ES2016的功能,例如**运算符。
同样,--target ES2017将指示编译器不要转换特定于ES2017的特性,如async/ await。
--target ESNext针对最新支持的ES提议功能。
以前,如果TypeScript无法确定变量的类型,则会选择any类型。
let x; // implicitly 'any'
let y = []; // implicitly 'any[]'
let z: any; // explicitly 'any'.
使用TypeScript 2.1,而不仅仅是选择any,TypeScript将根据您最后分配的内容推断类型。
仅在设置--noImplicitAny时才启用此选项。
let x;
// You can still assign anything you want to 'x'.
x = () => 42;
// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.
let y = x();
// Thanks to that, it will now tell you that you can't add a number to a function!
console.log(x + y);
// ~~~~~
// Error! Operator '+' cannot be applied to types '() => number' and 'number'.
// TypeScript still allows you to assign anything you want to 'x'.
x = "Hello world!";
// But now it also knows that 'x' is a 'string'!
x.toLowerCase();
现在也对空数组进行了相同类型的跟踪。
声明为没有类型注释且初始值为[]的变量被视为隐式any[]变量。然而,每个后续的x.push(value),x.unshift(value)或者x[n] = value操作根据添加到的元素来演变变量的类型。
function f1() {
let x = [];
x.push(5);
x[1] = "hello";
x.unshift(true);
return x; // (string | number | boolean)[]
}
function f2() {
let x = null;
if (cond()) {
x = [];
while (cond()) {
x.push("hello");
}
}
return x; // string[] | null
}
这样做的一个很大好处是,在运行--noImplicitAny时你会看到更少的隐式any错误。仅当编译器无法知道没有类型注释的变量类型时,才会报告隐式any错误。
function f3() {
let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.
x.push(5);
function g() {
x; // Error: Variable 'x' implicitly has an 'any[]' type.
}
}
字符串,数字和布尔文字类型(例如"abc",1和true)仅在存在显式类型注释时才推断。从TypeScript 2.1开始,始终为const变量和readonly属性推断文字类型。
为没有类型注释的const变量或readonly属性推断的类型是文字初始值设定项的类型。为具有初始值设定项且没有类型注释的let变量,var变量,参数或非readonly属性推断的类型是初始化程序的扩展文字类型。对于字符串文字类型的加宽类型是string,number对于数字文字类型,boolean对于true或false,以及包含枚举文字类型的枚举。
const c1 = 1; // Type 1
const c2 = c1; // Type 1
const c3 = "abc"; // Type "abc"
const c4 = true; // Type true
const c5 = cond ? 1 : "abc"; // Type 1 | "abc"
let v1 = 1; // Type number
let v2 = c2; // Type number
let v3 = c3; // Type string
let v4 = c4; // Type boolean
let v5 = c5; // Type number | string
可以通过显式类型注释来控制文字类型扩展。具体来说,当为没有类型注释的const位置推断出文字类型的表达式时,该const变量将推断出一个加宽的文字类型。但是,当const位置具有显式文字类型注释时,该const变量将获得非加宽文字类型。
const c1 = "hello"; // Widening type "hello"
let v1 = c1; // Type string
const c2: "hello" = "hello"; // Type "hello"
let v2 = c2; // Type "hello"
在ES2015中,返回对象的构造函数隐式地将this值替换为super()的任何调用者。因此,有必要捕获super()的任何潜在返回值,并且使用this替换它。此更改允许使用Custom Elements,该元素利用此特性用用户编写的构造函数初始化浏览器分配的元素。
class Base {
x: number;
constructor() {
// return a new object other than `this`
return {
x: 1,
};
}
}
class Derived extends Base {
constructor() {
super();
this.x = 2;
}
}
输出:
var Derived = (function (_super) {
__extends(Derived, _super);
function Derived() {
var _this = _super.call(this) || this;
_this.x = 2;
return _this;
}
return Derived;
}(Base));
这种变化导致扩展内置类(如,Error,Array,Map,等)的行为中断。
通常,一个项目有多个输出目标,例如ES5和ES2015,调试和生产,CommonJS和System;这两个目标之间只有少数配置选项发生变化,维护多个tsconfig.json文件可能很麻烦。
TypeScript 2.1支持使用extends继承配置,其中:
configs/base.json:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
tsconfig.json:
{
"extends": "./configs/base",
"files": [
"main.ts",
"supplemental.ts"
]
}
tsconfig.nostrictnull.json:
{
"extends": "./tsconfig",
"compilerOptions": {
"strictNullChecks": false
}
}
使用--alwaysStrict调用编译器的原因:
模块在严格模式下自动解析。对于非模块代码,建议使用新标志。