日常类型:
TypeScript
中的常用类型
The primitives: string, number, and boolean
基本类型:
string
,number
, 和boolean
JavaScript
有三种非常常用的基本类型string
,number
, 和boolean
,这些类型分别对应TypeScript
中的三种基本类型
string
表示字符串,如:'Hello'
number
表示数字,如:1,2,3
boolean
用于两个值true
和false
类型名称
String
、Number
和Boolean
是合法的,但它们指的是一些很少出现在您代码中的特殊内置类型。始终使用string
、number
或boolean
作为类型。
String、Number
和Boolean
类型分别对应JavaScript
中的String、Number
和Boolean
对象。这些对象是JavaScript
中原始字符串、数字和布尔值的包装对象,它们提供了一些额外的方法和属性。
Arrays
数组
要指定像[1,2,3]
这样的数组类型,可以使用语法number[]
,这种语法适用于任何类型(例如: string[]
是字符串数组,依次类推).此外,还可以使用Array<类型名称>
的语法来指定数组的类型,它们的意思是一样的
any
可以在不希望特定值引起类型检查错误时使用它
当一个值的类型为any
时,可以访问它的任何属性或者几乎任何语法上合法的其他操作,而不会引发类型检查错误
当你不想编写冗长的类型来说服TypeScript
某行代码是正确的,any
类型很有用
当您没有指定类型,且 TypeScript
无法从上下文推断出类型时,编译器通常会默认为 any
。
不过,您通常希望避免这种情况,因为 any
不会进行类型检查。使用编译器标志 noImplicitAny
来将任何隐式的 any
标记为错误。
在实际开发中,应尽量避免使用
any
类型。这是因为any
类型会关闭TypeScript
的类型检查功能,使您无法在编译时发现潜在的错误。
Type Annotations on Variables
变量上的类型注释
类型注释是一种语法,它允许您显式指定变量,函数参数和返回值等内容的类型
当你const
,var
或let
声明变量时,可以选择添加类型注释来显式指定变量的类型
let myName: string = 'lisi'
然而,在大多数情况下,这并不是必须的.只要有可能,TypeScript
会尝试自动推断您代码中的类型.例如,变量的类型是根据其初始化器的类型推断出来的
大多数情况下,您不需要显式学习推断规则。如果您刚开始使用 TypeScript
,可以尝试使用比您想象的更少的类型注释 - 您可能会惊讶于 TypeScript
能够完全理解您的代码所需的类型注释数量之少。
Functions
函数
函数是在JavsScript
中传递数据的主要方式.TypeScript
允许您指定函数的输入和输出值的类型
Parameter Type Annotations
参数类型注释
当你声明一个函数,你可以在每个参数后添加类型注释,以声明该函数接受哪些类型的参数.参数类型注释在参数名词后面:
function greet(name: string) {
console.log('Hello,' + name.toUpperCase()+ '!!')
}
当参数有了类型注释,将检查传递给该函数的参数
greet(42);
// Argument of type 'number' is not assignable to parameter of type 'string'.
即使您没有在参数上添加类型注释,
TypeScript
仍然会检查您传递的参数数量是否正确
Return Type Annotations
返回类型注释
您也可以添加返回类型注释。返回类型注释出现在参数列表之后。
function getFavoriteNumber(): number {
return 26;
}
就像变量类型注释一样,您通常不需要返回类型注释,因为 TypeScript
会根据函数的返回语句推断函数的返回类型。上面示例中的类型注释并没有改变任何东西。有些代码库会为文档目的、防止意外更改或仅出于个人喜好而显式指定返回类型。
Anonymous Functions
匿名函数
匿名函数与函数声明有一些不同.当一个函数出现在TypeScript
可以确定它将如何被调用的地方时,该函数的参数会自动获得类型
const names = ["Alice", "Bob", "Eve"];
names.forEach(function (s) {
console.log(s.toUppercase());
});
names.forEach((s) => {
console.log(s.toUppercase());
});
即使参数 s
没有类型注释,TypeScript
也会使用 forEach
函数的类型以及数组的推断类型来确定 s
的类型。
这个过程被称为上下文类型,因为函数出现的上下文决定了它应该具有的类型。
Object Types
对象类型
除了基本类型之外,您最常遇到的类型是对象类型。这指的是具有属性的任何 JavaScript
值,几乎所有的值都是这样!要定义一个对象类型,我们只需列出它的属性及其类型。
例如,这是一个接受类似点的对象的函数:
// 参数的类型注释是一个对象类型
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
在这里,我们用一个具有两个属性的对象类型注释了参数x
和 y
, 它们都是 number
类型。您可以使用 ,
或 ;
分隔属性,最后一个分隔符是可选的。
每个属性的类型部分也是可选的。如果您不指定类型,它将被假定为any
类型。
Optional Properties
可选属性
对象类型还可以指定其部分或全部属性是可选的。要做到这一点,在属性名称后面添加一个 ?
。
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
在 JavaScript
中,如果您访问一个不存在的属性,您将获得 undefined
值而不是运行时错误。因此,当您从可选属性中读取时,您必须在使用它之前检查 undefined
。
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
// 'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}
Union Types
联合类型
TypeScript
的类型系统允许您使用多种运算符从现有类型构建新类型。现在我们已经知道如何编写一些类型,是时候开始以有趣的方式组合它们了。
Defining a Union Type
定义联合类型
组合类型的第一种方法是使用联合类型。联合类型是由两个或多个其他类型组成的类型,表示值可以是这些类型中的任何一个。我们将这些类型称为联合的成员。
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Working with Union Types
使用联合类型
提供与联合类型匹配的值很容易 - 只需提供与联合的任何成员匹配的类型即可。如果您拥有联合类型的值,那么如何处理它呢?
TypeScript
只允许对联合的每个成员都有效的操作。例如,如果您有 string | number
联合类型,那么您不能使用仅在 string
类型上可用的方法。
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
function printId(id: number | string) {
console.log(id.toUpperCase());
}
解决方案是使用代码缩小联合,就像您在没有类型注释的 JavaScript
中所做的那样。缩小发生在 TypeScript
根据代码的结构推断出更具体的类型时。这意味着您可以使用代码来检查值的类型,并根据检查结果执行不同的操作。
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
另一个例子是使用Array.isArray
这样的函数:
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
注意,在 else
分支中,我们不需要做任何特殊的事情 - 如果 x
不是字符串数组,那么它一定是一个字符串。
有时您会遇到一个联合类型,其中所有成员都具有某些共同点。例如,数组和字符串都具有 slice
方法。如果联合的每个成员都具有一个共同的属性,那么您可以在不缩小的情况下使用该属性。
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
联合类型就像一个大容器,它可以容纳多种不同类型的值。但是当你想对这些值进行操作时,你只能进行所有成员都共有的操作
Type Aliases
类型别名
我们一直直接在类型注释中编写对象类型和联合类型。这很方便,但通常我们希望多次使用相同的类型并用单个名称引用它。
类型别名就是这样 - 任何类型的名称。类型别名的语法是:
type Point = {
x: number;
y: number;
};
// Exactly the same as the earlier example
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
实际上,您可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型。
type ID = number | string;
请注意,别名只是别名 - 您不能使用类型别名来创建相同类型的不同“版本”。
通常指的是无法直接修改或扩展现有的类型别名,以创建具有不同行为或属性的新类型。类型别名只是给现有类型起一个别名,本质上是同一个类型。
// 假设我们有一个类型别名MyString,表示字符串类型:
type MyString = string;
// 现在,如果我们尝试使用类型别名来创建一个新的版本,例如将字符串类型限制为只包含小写字母,我们无法通过类型别名直接实现:
type LowercaseString = MyString; // 无法通过类型别名直接创建新的版本
// 要实现此目标,需要使用其他特性,比如自定义类型或接口,或者使用类型转换函数等:
type LowercaseString = string;
function toLowercaseString(value: string): LowercaseString {
return value.toLowerCase();
}
// 在上面的例子中,我们定义了一个名为LowercaseString的自定义类型,用于表示只包含小写字母的字符串。然后,我们编写了一个函数toLowercaseString,它接受一个普通的字符串作为参数,并将其转换为LowercaseString类型。
当您使用别名时,它就像您已经写过的别名类型一样。
这句话的意思是,当你使用类型别名时,它和直接使用被别名的类型是完全等价的。编译器会将类型别名替换为被别名的实际类型,就好像你直接写了被别名的类型一样。
type MyNumber = number;
function addNumbers(a: MyNumber, b: MyNumber): MyNumber {
return a + b;
}
// 在这个例子中,虽然我们使用了类型别名MyNumber,但它实际上就是number类型的别名。因此,当我们调用addNumbers函数并传递两个参数时,编译器会将MyNumber替换为number,并执行函数的求和操作。
换句话说,这段代码看起来可能是非法的,但根据 TypeScript
是可以的,因为两种类型都是相同类型的别名。
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
// Create a sanitized input
let userInput = sanitizeInput(getInput());
// Can still be re-assigned with a string though
userInput = "new input";
type userInput = string;
let input: userInput = "hello";
input = "world"; // OK
input = 42; // Error: Type 'number' is not assignable to type 'string'.
由于 userInput
是 string
类型的别名,所以我们可以将任何字符串赋给变量 input
,但不能将其他类型的值赋给它。
类型别名不能重复
type userInput = string;
type userInput = number; // Cannot redeclare block-scoped variable 'userInput'.
Interfaces
接口
接口声明是命名对象类型的另一种方式。
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
TypeScript
是一种结构化类型系统,这意味着它只关心值的结构和能力,而不关心它们如何命名或定义。这意味着,当您使用接口或类型别名定义对象类型时,TypeScript
只关心对象是否具有预期的属性。
Differences Between Type Aliases and Interfaces
类型别名和接口之间的区别
类型别名和接口非常相似,在许多情况下,您可以自由选择它们。接口的几乎所有功能都在类型别名中可用,关键区别在于类型别名不能重新打开以添加新属性,而接口始终是可扩展的。
// 接口扩展
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
// 类型别名扩展
type Animal = {
name: string;
};
type Bear = Animal & {
honey: boolean;
};
const bear = getBear();
bear.name;
bear.honey;
它们之间的主要区别在于:
-
在
TypeScript 4.2
版本之前,类型别名名称可能会出现在错误消息中,有时会代替等效的匿名类型。而接口将始终在错误消息中命名 -
类型别名不能参与声明合并,但接口可以。这意味着您可以通过扩展现有接口来添加新属性。
// 接口可以添加新值
interface Mammal {
genus: string;
}
interface Mammal {
breed: string;
}
const animal: Mammal = {
genus: "1234",
breed: "232",
};
// 类型别名不可以
type Reptile = {
genus: string;
};
type Reptile = {
breed: string;
};
// Error: Duplicate identifier 'Reptile'.
- 接口只能用于声明对象的形状,不能重命名基本类型。这意味着您不能使用接口来为基本类型(如
string
或number
)定义新名称。
interface AnObject1 {
value: string;
}
type AnObject2 = {
value: string;
};
// 对于现有的基本类型,使用type我们可以创建自定义名称
type SanitizedString = string;
type EvenNumber = number;
// 这在接口上是不可行的
interface X extends string {}
- 接口名称将始终以其原始形式出现在错误消息中,但仅当它们被名称使用时。
当您使用接口时,如果发生错误,错误消息中将始终显示接口的名称.接口名称仅在按名称使用时才会出现在错误消息中。如果您使用匿名类型,则错误消息中不会显示接口名称。
interface Mammal {
name: string;
}
function echoMammal(m: Mammal) {
console.log(m.name);
}
// 错误消息中将显示接口的名称
echoMammal({ name: 12343 });
// 由于它没有直接命名为TypeScript不会在错误消息中提到它
function echoAnimal(m: { name: string }) {
console.log(m.name);
}
echoAnimal({ name: 12345 });
大多数情况下,您可以根据个人喜好选择使用类型别名还是接口。如果您不确定应该使用哪种方式,可以先使用接口,直到您需要使用类型别名提供的功能。
Type Assertions
类型断言
是一种告诉 TypeScript 您比它更了解值类型的方法。它允许您指定一个更具体的类型,而不是
TypeScript
推断出的类型
有时您会拥有 TypeScript
无法知道的值类型的信息。
例如: 如果您使用 document.getElementById
,TypeScript
只知道这将返回某种 HTMLElement
,但您可能知道您的页面上总是会有一个具有给定 ID
的 HTMLCanvasElement
。
在这种情况下,您可以使用类型断言来指定更具体的类型。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
类型断言就像类型注释一样,它们会被编译器删除,不会影响代码的运行时行为。
您还可以使用尖括号语法.这两种语法是等效的。但是,如果您的代码位于 .tsx
文件中,则不能使用尖括号语法,因为它与 JSX
语法冲突。
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
TypeScript
只允许将类型断言转换为更具体或更不具体的类型版本。这条规则防止了像这样的“不可能”的强制转换:
const x = "hello" as number;
// 将类型` string `转换为类型` number `可能是一个错误,因为两种类型都没有足够的重叠。如果这是故意的,请先将表达式转换为` unknown `。
有时这条规则可能过于保守,不允许更复杂的可能有效的强制转换。如果发生这种情况,您可以使用两个断言,先转换为 any
(或稍后将介绍的 unknown
),然后再转换为所需的类型。
const a = (expr as any) as T;
Literal Types
字面量类型
除了一般的string
和number
类型外,我们还可以在类型位置上引用特定的字符串和数字。
考虑这一点的一种方法是考虑 JavaScript
如何提供不同的方式来声明变量。var
和 let
都允许更改变量内部保存的内容,而 const
则不允许。这反映在 TypeScript
如何为字面量创建类型。
let changingString = "Hello World";
changingString = "Olá Mundo";
// let changingString: string
// 因为`changingString`可以表示任何可能的字符串,所以
// TypeScript在类型系统中描述它为stirng
const constantString = "Hello World";
// const constantString: "Hello World"
// 因为`constantString`只能表示一个可能的字符串
// 所以有字面量类型表示
单独使用时,字面量类型并不是非常有价值的。
let x: "hello" = "hello";
// OK
x = "hello";
// Error
x = "howdy";
拥有一个只能有一个值的变量并没有太大用处!
但是,通过将字面量组合成联合,您可以表达更有用的概念 - 例如,只接受某些已知值集合的函数。
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// 类型为` "centre" `的参数不能赋值给类型为` "left" | "right" | "center" `的参数。
数值字面量类型的工作方式相同:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
当然,你可以将它们与非字面量类型(non-literal types
)结合使用:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
// 类型为` "automatic" `的参数不能赋值给类型为` Options | "auto" `的参数。
还有一种字面量类型:布尔字面量。只有两种布尔字面量类型,正如您可能猜到的,它们分别是 true
和 false
类型。boolean
类型本身实际上只是 true | false
联合的别名。
Literal Inference
字面量推断
当您使用对象初始化变量时,TypeScript
假定该对象的属性可能会在以后更改值。例如,如果您编写了这样的代码:
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
TypeScript
不认为将 1
分配给先前为 0
的字段是错误的。换句话说,obj.counter
必须具有 number
类型,而不是 0
,因为类型用于确定读写行为。
对字符串也适用相同的规则:
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// ` string `类型的参数不能赋值给` "GET" | "POST" `类型的参数。
在上面的示例中,req.method
被推断为 string
,而不是 “GET”
。因为在创建 req
和调用 handleRequest
之间可以评估代码,这可能会将新字符串 “GUESS”
分配给 req.method
,所以 TypeScript
认为此代码存在错误。
有两种方法可以解决这个问题。
- 您可以通过在任一位置添加类型断言来更改推断:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");
Change 1
的意思是“我打算让req.method
始终具有字面类型“GET”
,从而防止在之后将“GUESS”
分配给该字段。” Change 2
的意思是“我因为其他原因知道 req.method 的值为“GET”
。
- 你可以使用
as const
将整个对象转换为类型字面量:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
as const
后缀的作用类似于const
,但用于类型系统,确保所有属性都分配了字面类型,而不是像字符串或数字这样的更通用版本。
null
andundefined
JavaScript
有两个原始值用于表示缺失或未初始化的值:null
和undefined
。
TypeScript
有两个相应的类型,名称相同。这些类型的行为取决于是否开启了strictNullChecks
选项。
strictNullChecks
off
如果关闭strictNullChecks
,可能为null
或undefined
的值仍然可以正常访问,并且可以将null
和undefined
的值分配给任何类型的属性。这类似于没有空值检查的语言(例如C#
,Java
)的行为。缺乏对这些值的检查往往是错误的主要来源;如果在他们的代码库中实际操作,我们总是建议人们打开strictNullChecks
。
strictNullChecks
on
当开启strictNullChecks
时,如果一个值为null
或undefined
,您需要在使用该值的方法或属性之前测试这些值。就像在使用可选属性之前检查undefined
一样,我们可以使用缩小来检查可能为null
的值。
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Non-null Assertion Operator
(Postfix !
)
非空断言运算符(后缀
!
)
TypeScript
还有一种特殊的语法,用于在不进行任何显式检查的情况下从类型中删除null
和undefined
。在任何表达式后写!
实际上是一种类型断言,表示该值不是null
或undefined
。
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
就像其他类型断言一样,这不会改变代码的运行时行为,因此只有在您知道该值不可能为null
或undefined
时才使用!非常重要。
Enums
枚举
枚举是TypeScript
添加到JavaScript
中的一项功能,它允许描述一个可能是一组可能的命名常量之一的值。与大多数TypeScript
功能不同,这不是对JavaScript
的类型级别添加,而是添加到语言和运行时中的东西。因此,这是一个您应该知道存在的功能,但除非您确定,否则可能会暂时不使用。您可以在枚举参考页面上阅读有关枚举的更多信息。
Less Common Primitives
不太常见的原始类型
值得一提的是,JavaScript
中其余的原始类型也在类型系统中表示。尽管我们不会在这里深入探讨。
bigint
从 ES2020
开始,JavaScript
中有一种用于表示非常大的整数的原始值 BigInt
:
// 使用BigInt函数创建一个bigint对象
const oneHundred: bigint = BigInt(100);
// 通过字面量语法创建一个BigInt
const anotherHundred: bigint = 100n;
你可以在TypeScript 3.2
的发行说明中了解有关BigInt的更多信息。
symbol
JavaScript
中有一个原始类型,用于通过Symbol()
函数创建全局唯一引用。
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这种比较似乎是无意的,因为` typeof firstName `和` typeof secondName `类型没有重叠。
// 不可能发生
}