本章解释了 TypeScript 的要点。
阅读本章后,您应该能够理解以下 TypeScript 代码
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}您可能认为这很神秘。我同意你的看法!但是(我希望证明)这种语法相对容易学习。一旦您理解了它,它就会为您提供代码行为的即时、精确和全面的摘要,而无需阅读冗长的英文描述。
可以通过多种方式配置 TypeScript 编译器。一组重要的选项控制编译器检查 TypeScript 代码的彻底程度。最大设置通过 `--strict` 激活,我建议始终使用它。这使得程序稍微难以编写,但我们也获得了静态类型检查的全部好处。
这就是您现在需要了解的有关 `--strict` 的全部内容
如果您想了解更多详细信息,请继续阅读。
将 `--strict` 设置为 `true` 会将以下所有选项设置为 `true`
在本书的后面,当我们开始使用 TypeScript 创建 npm 包 和 Web 应用程序 时,我们将看到更多编译器选项。TypeScript 手册有关于它们的全面文档。
在本章中,类型只是一组值。JavaScript 语言(不是 TypeScript!)只有八种类型
所有这些类型都是*动态的*:我们可以在运行时使用它们。
TypeScript 为 JavaScript 带来了一个额外的层:*静态类型*。这些仅在编译或类型检查源代码时存在。每个存储位置(变量、属性等)都有一个静态类型,用于预测其动态值。类型检查可确保这些预测成真。
并且有很多东西可以*静态地*检查(无需运行代码)。例如,如果函数 `toString(num)` 的参数 `num` 具有静态类型 `number`,则函数调用 `toString('abc')` 是非法的,因为参数 `'abc'` 具有错误的静态类型。
function toString(num: number): string {
return String(num);
}前面的函数声明中有两个类型注解
`number` 和 `string` 都是*类型表达式*,用于指定存储位置的类型。
通常,如果没有类型注解,TypeScript 可以*推断*静态类型。例如,如果我们省略 `toString()` 的返回类型,TypeScript 会推断它是 `string`
// %inferred-type: (num: number) => string
function toString(num: number) {
return String(num);
}类型推断不是猜测:它遵循明确的规则(类似于算术)来推导未明确指定的类型。在这种情况下,return 语句将函数 `String()`(将任意值映射到字符串)应用于类型为 `number` 的值 `num` 并返回结果。这就是推断的返回类型为 `string` 的原因。
如果既未明确指定也无法推断位置的类型,则 TypeScript 会对其使用类型 `any`。这是所有值的类型和通配符,因为如果值具有该类型,我们可以做任何事情。
使用 `--strict` 时,仅当我们显式使用 `any` 时才允许使用它。换句话说:每个位置都必须具有显式或推断的静态类型。在以下示例中,参数 `num` 两者都没有,并且我们收到编译时错误
// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
return String(num);
}类型注解冒号后的类型表达式从简单到复杂,创建如下。
基本类型是有效的类型表达式
symbolobject.有很多方法可以组合基本类型来产生新的*复合类型*。例如,通过*类型运算符*,它们组合类型的方式类似于集合运算符*并集* (`∪`) 和*交集* (`∩`) 组合集合的方式。我们很快就会看到如何做到这一点。
TypeScript 有两种语言级别
我们可以在语法中看到这两个级别
const undef: undefined = undefined;在动态级别,我们使用 JavaScript 声明一个变量 `undef` 并使用值 `undefined` 初始化它。
在静态级别,我们使用 TypeScript 指定变量 `undef` 具有静态类型 `undefined`。
请注意,相同的语法 `undefined` 意味着不同的东西,具体取决于它是在动态级别还是静态级别使用。
尝试培养对两种语言级别的意识
这非常有助于理解 TypeScript。
使用 `type` 我们可以为现有类型创建一个新名称(别名)
type Age = number;
const age: Age = 82;数组在 JavaScript 中扮演两个角色(一个或两个)
有两种方法可以表示数组 `arr` 被用作其元素都是数字的列表
let arr1: number[] = [];
let arr2: Array<number> = [];通常,如果有赋值,TypeScript 可以推断变量的类型。在这种情况下,我们实际上必须帮助它,因为对于空数组,它无法确定元素的类型。
我们稍后会回到尖括号表示法 (`Array<number>`)。
如果我们将二维点存储在数组中,那么我们将该数组用作元组。如下所示
let point: [number, number] = [7, 5];数组作为元组需要类型注解,因为对于数组字面量,TypeScript 推断列表类型,而不是元组类型
// %inferred-type: number[]
let point = [7, 5];`Object.entries(obj)` 的结果是元组的另一个示例:一个数组,其中包含 `obj` 的每个属性的一个 [键,值] 对。
// %inferred-type: [string, number][]
const entries = Object.entries({ a: 1, b: 2 });
assert.deepEqual(
entries,
[[ 'a', 1 ], [ 'b', 2 ]]);推断的类型是一个元组数组。
这是一个函数类型的示例
(num: number) => string此类型包含接受单个数字类型参数并返回字符串的每个函数。让我们在类型注解中使用此类型
const toString: (num: number) => string = // (A)
(num: number) => String(num); // (B)通常,我们必须为函数指定参数类型。但在这种情况下,可以从 A 行的函数类型推断出 B 行中 `num` 的类型,我们可以省略它
const toString: (num: number) => string =
(num) => String(num);如果我们省略 `toString` 的类型注解,TypeScript 会从箭头函数推断出一个类型
// %inferred-type: (num: number) => string
const toString = (num: number) => String(num);这一次,`num` 必须有一个类型注解。
以下示例更复杂
function stringify123(callback: (num: number) => string) {
return callback(123);
}我们正在使用函数类型来描述 `stringify123()` 的参数 `callback`。由于此类型注解,TypeScript 拒绝以下函数调用。
// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
// Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);但它接受此函数调用
assert.equal(
stringify123(String), '123');TypeScript 通常可以推断函数的返回类型,但允许显式指定它们,并且偶尔有用(至少,它没有任何坏处)。
对于 `stringify123()`,指定返回类型是可选的,如下所示
function stringify123(callback: (num: number) => string): string {
return callback(123);
}`void` 是函数的特殊返回类型:它告诉 TypeScript 函数始终返回 `undefined`。
它可以显式地这样做
function f1(): void {
return undefined;
}或者它可以隐式地这样做
function f2(): void {}但是,这样的函数不能显式返回 `undefined` 以外的值
function f3(): void {
// @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
return 'abc';
}标识符后的问号表示该参数是可选的。例如
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
callback = String;
}
return callback(123); // (A)
}只有在我们确保 `callback` 不是 `undefined`(如果省略了参数,则为 `undefined`)的情况下,TypeScript 才允许我们进行 A 行中的函数调用。
TypeScript 支持 参数默认值
function createPoint(x=0, y=0): [number, number] {
return [x, y];
}
assert.deepEqual(
createPoint(),
[0, 0]);
assert.deepEqual(
createPoint(1, 2),
[1, 2]);默认值使参数可选。我们通常可以省略类型注释,因为 TypeScript 可以推断类型。例如,它可以推断出 x 和 y 都具有 number 类型。
如果我们想添加类型注释,则如下所示。
function createPoint(x:number = 0, y:number = 0): [number, number] {
return [x, y];
}我们也可以在 TypeScript 参数定义中使用 剩余参数。它们的静态类型必须是数组(列表或元组)
function joinNumbers(...nums: number[]): string {
return nums.join('-');
}
assert.equal(
joinNumbers(1, 2, 3),
'1-2-3');变量持有的值(一次一个值)可能是不同类型的成员。在这种情况下,我们需要一个联合类型。例如,在以下代码中,stringOrNumber 是 string 类型或 number 类型
function getScore(stringOrNumber: string|number): number {
if (typeof stringOrNumber === 'string'
&& /^\*{1,5}$/.test(stringOrNumber)) {
return stringOrNumber.length;
} else if (typeof stringOrNumber === 'number'
&& stringOrNumber >= 1 && stringOrNumber <= 5) {
return stringOrNumber
} else {
throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
}
}
assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);stringOrNumber 的类型为 string|number。类型表达式 s|t 的结果是类型 s 和 t 的集合论并集(解释为集合)。
undefined 和 null在许多编程语言中,null 是所有对象类型的一部分。例如,每当 Java 中变量的类型为 String 时,我们都可以将其设置为 null,并且 Java 不会报错。
相反,在 TypeScript 中,undefined 和 null 由单独的、不相交的类型处理。如果我们想允许它们,则需要联合类型,例如 undefined|string 和 null|string
let maybeNumber: null|number = null;
maybeNumber = 123;否则,我们会收到错误
// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
let maybeNumber: number = null;
maybeNumber = 123;请注意,TypeScript 不会强制我们立即初始化(只要我们不在初始化之前从变量中读取数据)。
let myNumber: number; // OK
myNumber = 123;回想一下之前提到的这个函数
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
callback = String;
}
return callback(123); // (A)
}让我们重写 stringify123(),以便参数 callback 不再是可选的:如果调用者不想提供函数,则必须显式传递 null。结果如下所示。
function stringify123(
callback: null | ((num: number) => string)) {
const num = 123;
if (callback === null) { // (A)
callback = String;
}
return callback(num); // (B)
}
assert.equal(
stringify123(null),
'123');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
assert.throws(() => stringify123());同样,我们必须先处理 callback 不是函数的情况(A 行),然后才能在 B 行进行函数调用。如果我们没有这样做,TypeScript 会在该行报告错误。
undefined|T以下三个参数声明非常相似
x?: numberx = 456x: undefined | number如果参数可选,则可以省略。在这种情况下,它的值为 undefined
function f1(x?: number) { return x }
assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit如果参数具有默认值,则在省略参数或将其设置为 undefined 时将使用该值
function f2(x = 456) { return x }
assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit如果参数具有联合类型,则不能省略,但我们可以将其设置为 undefined
function f3(x: undefined | number) { return x }
assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit与数组类似,对象在 JavaScript 中扮演着两个角色(有时会混合使用)
记录:在开发时已知的固定数量的属性。每个属性可以具有不同的类型。
字典:任意数量的属性,其名称在开发时未知。所有属性都具有相同的类型。
我们在本章中忽略了作为字典的对象——它们在 §15.4.5 “索引签名:作为字典的对象” 中介绍。顺便说一句,地图通常是字典的更好选择。
接口描述作为记录的对象。例如
interface Point {
x: number;
y: number;
}我们也可以用逗号分隔成员
interface Point {
x: number,
y: number,
}TypeScript 类型系统的一大优势是它以结构化方式工作,而不是以名义化方式工作。也就是说,接口 Point 匹配具有适当结构的所有对象
interface Point {
x: number;
y: number;
}
function pointToString(pt: Point) {
return `(${pt.x}, ${pt.y})`;
}
assert.equal(
pointToString({x: 5, y: 7}), // compatible structure
'(5, 7)');相反,在 Java 的名义类型系统中,我们必须为每个类显式声明它实现的接口。因此,一个类只能实现其创建时存在的接口。
对象字面量类型是匿名接口
type Point = {
x: number;
y: number;
};对象字面量类型的一个好处是可以内联使用它们
function pointToString(pt: {x: number, y: number}) {
return `(${pt.x}, ${pt.y})`;
}如果可以省略属性,我们在其名称后放一个问号
interface Person {
name: string;
company?: string;
}在以下示例中,john 和 jane 都匹配接口 Person
const john: Person = {
name: 'John',
};
const jane: Person = {
name: 'Jane',
company: 'Massive Dynamic',
};接口也可以包含方法
interface Point {
x: number;
y: number;
distance(other: Point): number;
}就 TypeScript 的类型系统而言,方法定义和值为函数的属性是等效的
interface HasMethodDef {
simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
simpleMethod: (flag: boolean) => void;
}
const objWithMethod: HasMethodDef = {
simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;
const objWithOrdinaryFunction: HasMethodDef = {
simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;
const objWithArrowFunction: HasMethodDef = {
simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;我的建议是使用最能表达如何设置属性的语法。
回想一下 TypeScript 的两种语言级别
类似地
普通函数存在于动态级别,是值的工厂,并具有表示值的形参。形参在括号之间声明
const valueFactory = (x: number) => x; // definition
const myValue = valueFactory(123); // use泛型存在于静态级别,是类型的工厂,并具有表示类型的形参。形参在尖括号之间声明
type TypeFactory<X> = X; // definition
type MyType = TypeFactory<string>; // use 命名类型参数
在 TypeScript 中,通常对类型参数使用单个大写字母(例如 T、I 和 O)。但是,允许使用任何合法的 JavaScript 标识符,并且更长的名称通常使代码更易于理解。
// Factory for types
interface ValueContainer<Value> {
value: Value;
}
// Creating one type
type StringContainer = ValueContainer<string>;Value 是一个类型变量。可以在尖括号之间引入一个或多个类型变量。
类也可以具有类型参数
class SimpleStack<Elem> {
#data: Array<Elem> = [];
push(x: Elem): void {
this.#data.push(x);
}
pop(): Elem {
const result = this.#data.pop();
if (result === undefined) {
throw new Error();
}
return result;
}
get length() {
return this.#data.length;
}
}类 SimpleStack 具有类型参数 Elem。当我们实例化类时,我们还为类型参数提供一个值
const stringStack = new SimpleStack<string>();
stringStack.push('first');
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');映射在 TypeScript 中是泛型类型化的。例如
const myMap: Map<boolean,string> = new Map([
[false, 'no'],
[true, 'yes'],
]);由于类型推断(基于 new Map() 的参数),我们可以省略类型参数
// %inferred-type: Map<boolean, string>
const myMap = new Map([
[false, 'no'],
[true, 'yes'],
]);函数定义可以像这样引入类型变量
function identity<Arg>(arg: Arg): Arg {
return arg;
}我们按如下方式使用该函数。
// %inferred-type: number
const num1 = identity<number>(123);由于类型推断,我们可以再次省略类型参数
// %inferred-type: 123
const num2 = identity(123);请注意,TypeScript 推断出类型 123,它是一个包含一个数字的集合,比类型 number 更具体。
箭头函数也可以具有类型参数
const identity = <Arg>(arg: Arg): Arg => arg;这是方法的类型参数语法
const obj = {
identity<Arg>(arg: Arg): Arg {
return arg;
},
};function fillArray<T>(len: number, elem: T): T[] {
return new Array<T>(len).fill(elem);
}类型变量 T 在此代码中出现四次
fillArray<T> 引入的。因此,它的作用域是函数。elem 的类型注释中首次使用。fillArray() 的返回类型。Array() 的类型参数。我们在调用 fillArray() 时可以省略类型参数(A 行),因为 TypeScript 可以从参数 elem 推断出 T
// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
arr1, ['*', '*', '*']);
// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)让我们使用我们所学的知识来理解我们之前看到的代码片段
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}这是元素类型为 T 的数组的接口
方法 .concat() 具有零个或多个参数(通过剩余参数定义)。每个参数的类型都为 T[]|T。也就是说,它要么是 T 值的数组,要么是单个 T 值。
方法 .reduce() 引入了它自己的类型变量 U。U 用于表示以下实体都具有相同类型
callback() 的参数 statecallback() 的结果.reduce() 的可选参数 firstState.reduce() 的结果除了 state 之外,callback() 还具有以下参数
element,它与数组元素具有相同的类型 Tindex;一个数字T 的 array