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