在本章中,我们将介绍在 JavaScript 代码中会遇到的最常见的一些值类型,并说明在 TypeScript 中描述这些类型的方法。这不是一个详尽的列表,后续章节将介绍更多命名和使用其他类型的方法。
类型还可以出现在比类型注释更多的地方。当我们了解类型本身时,我们还将了解可以在其中引用这些类型以形成新构造的地方。
我们首先回顾一下编写 JavaScript 或 TypeScript 代码时可能会遇到的最基本且最常见的类型。这些类型稍后将构成更复杂类型的核心构建块。
基元:string
、number
和 boolean
JavaScript 有三个非常常用的 基元:string
、number
和 boolean
。每个在 TypeScript 中都有对应的类型。正如你所料,这些名称与使用 JavaScript typeof
运算符对这些类型的某个值时看到的名称相同
string
表示字符串值,例如"Hello, world"
number
用于表示数字,例如42
。JavaScript 没有针对整数的特殊运行时值,因此没有等效于int
或float
的类型——所有内容都只是number
boolean
用于表示两个值true
和false
类型名称
String
、Number
和Boolean
(以大写字母开头)是合法的,但它们指的是一些特殊的内置类型,这些类型在你的代码中很少出现。始终对类型使用string
、number
或boolean
。
数组
要指定数组的类型,例如 [1, 2, 3]
,可以使用语法 number[]
;此语法适用于任何类型(例如 string[]
是字符串数组,依此类推)。你还可以看到它写成 Array<number>
,其含义相同。我们将在介绍泛型时详细了解语法 T<U>
。
请注意,
[number]
是另一回事;请参阅 元组部分。
any
TypeScript 还有一种特殊类型,any
,在你不希望某个值导致类型检查错误时可以使用。
当一个值属于类型 any
时,你可以访问它的任何属性(这些属性反过来也属于类型 any
),像调用函数一样调用它,将其赋值给(或从)任何类型的某个值,或者执行其他任何语法上合法的事情
tsTry
letobj : any = {x : 0 };// None of the following lines of code will throw compiler errors.// Using `any` disables all further type checking, and it is assumed// you know the environment better than TypeScript.obj .foo ();obj ();obj .bar = 100;obj = "hello";constn : number =obj ;
当你不希望写出较长的类型来让 TypeScript 相信某一行代码是正确的时,any
类型非常有用。
noImplicitAny
当你未指定类型,并且 TypeScript 无法从上下文中推断出类型时,编译器通常会默认为 any
。
不过,你通常希望避免这种情况,因为 any
不会进行类型检查。使用编译器标志 noImplicitAny
将任何隐式 any
标记为错误。
变量上的类型注释
当你使用 const
、var
或 let
声明变量时,可以选择添加类型注释来明确指定变量的类型
tsTry
letmyName : string = "Alice";
TypeScript 不使用“左侧类型”风格的声明,如
int x = 0;
类型注释始终位于待类型化的内容之后。
不过,在大多数情况下,这是不需要的。在可能的情况下,TypeScript 会尝试自动推断代码中的类型。例如,变量的类型是根据其初始化程序的类型推断出来的
tsTry
// No type annotation needed -- 'myName' inferred as type 'string'letmyName = "Alice";
在大多数情况下,你不需要明确学习推断规则。如果你刚开始,请尝试使用比你认为需要的更少的类型注释 - 你可能会惊讶于你只需要很少的类型注释就能让 TypeScript 完全理解正在发生的事情。
函数
函数是 JavaScript 中传递数据的主要方式。TypeScript 允许你指定函数的输入和输出值的类型。
参数类型注释
声明函数时,可以在每个参数后添加类型注释,以声明函数接受的参数类型。参数类型注释位于参数名称之后
tsTry
// Parameter type annotationfunctiongreet (name : string) {console .log ("Hello, " +name .toUpperCase () + "!!");}
当参数具有类型注释时,将检查该函数的参数
tsTry
// Would be a runtime error if executed!Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.greet (42 );
即使参数没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。
返回类型注释
您还可以添加返回类型注释。返回类型注释出现在参数列表之后
tsTry
functiongetFavoriteNumber (): number {return 26;}
与变量类型注释非常相似,您通常不需要返回类型注释,因为 TypeScript 会根据其 return
语句推断函数的返回类型。上述示例中的类型注释不会改变任何内容。一些代码库将明确指定返回类型以用于文档目的,防止意外更改,或仅仅出于个人偏好。
返回 Promise 的函数
如果您想注释返回 Promise 的函数的返回类型,则应使用 Promise
类型
tsTry
async functiongetFavoriteNumber ():Promise <number> {return 26;}
匿名函数
匿名函数与函数声明有点不同。当函数出现在 TypeScript 可以确定其调用方式的位置时,该函数的参数将自动获得类型。
以下是一个示例
tsTry
constnames = ["Alice", "Bob", "Eve"];// Contextual typing for function - parameter s inferred to have type stringnames .forEach (function (s ) {console .log (s .toUpperCase ());});// Contextual typing also applies to arrow functionsnames .forEach ((s ) => {console .log (s .toUpperCase ());});
即使参数 s
没有类型注释,TypeScript 也使用了 forEach
函数的类型以及数组的推断类型来确定 s
将具有的类型。
此过程称为上下文类型化,因为函数所在的上下文会告知其应具有的类型。
与推断规则类似,您无需明确了解此过程如何发生,但了解它确实发生可以帮助您注意到何时不需要类型注释。稍后,我们将看到更多有关值所在的上下文如何影响其类型的示例。
对象类型
除了基本类型之外,您将遇到的最常见的类型是对象类型。这指的是具有属性的任何 JavaScript 值,几乎所有值都是如此!要定义对象类型,我们只需列出其属性及其类型。
例如,这是一个采用类似于点的对象作为参数的函数
tsTry
// The parameter's type annotation is an object typefunctionprintCoord (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
。
可选属性
对象类型还可以指定其部分或全部属性为可选。为此,在属性名称后添加一个?
tsTry
functionprintName (obj : {first : string;last ?: string }) {// ...}// Both OKprintName ({first : "Bob" });printName ({first : "Alice",last : "Alisson" });
在 JavaScript 中,如果你访问一个不存在的属性,你将获得值undefined
,而不是运行时错误。因此,当你从可选属性中读取时,你必须在使用之前检查undefined
。
tsTry
functionprintName (obj : {first : string;last ?: string }) {// Error - might crash if 'obj.last' wasn't provided!'obj.last' is possibly 'undefined'.18048'obj.last' is possibly 'undefined'.console .log (obj .last .toUpperCase ());if (obj .last !==undefined ) {// OKconsole .log (obj .last .toUpperCase ());}// A safe alternative using modern JavaScript syntax:console .log (obj .last ?.toUpperCase ());}
联合类型
TypeScript 的类型系统允许你使用各种运算符从现有类型构建新类型。现在我们知道了如何编写一些类型,是时候开始以有趣的方式组合它们了。
定义联合类型
你可能看到的组合类型的第一个方法是联合类型。联合类型是由两个或更多其他类型形成的类型,表示可能为其中任何一个类型的值。我们将这些类型中的每一个称为联合的成员。
让我们编写一个可以在字符串或数字上运行的函数
tsTry
functionprintId (id : number | string) {console .log ("Your ID is: " +id );}// OKprintId (101);// OKprintId ("202");// ErrorArgument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.2345Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.printId ({myID : 22342 });
使用联合类型
提供与联合类型匹配的值很容易 - 只需提供与联合的任何成员匹配的类型。如果您拥有联合类型的值,您将如何使用它?
TypeScript 仅允许在联合的每个成员有效的情况下执行操作。例如,如果您有联合 string | number
,则不能使用仅在 string
上可用的方法
tsTry
functionprintId (id : number | string) {Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.2339Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.console .log (id .()); toUpperCase }
解决方案是用代码缩小联合,就像在没有类型注释的情况下在 JavaScript 中所做的那样。当 TypeScript 可以根据代码结构为值推断出更具体的类型时,就会发生缩小。
例如,TypeScript 知道只有 string
值才会有 typeof
值 "string"
tsTry
functionprintId (id : number | string) {if (typeofid === "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
的函数
tsTry
functionwelcomePeople (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
不是 string[]
,那么它一定是 string
。
有时您会遇到所有成员都有一些共同点的联合。例如,数组和字符串都具有 slice
方法。如果联合中的每个成员都有一个公共属性,则可以在不缩小的情况下使用该属性
tsTry
// Return type is inferred as number[] | stringfunctiongetFirstThree (x : number[] | string) {returnx .slice (0, 3);}
令人困惑的是,类型的联合似乎具有这些类型的属性的交集。这不是偶然 - 名称联合来自类型理论。联合
number | string
是通过获取每个类型的值的联合来组成的。请注意,给定两个具有关于每个集合的相应事实的集合,只有这些事实的交集适用于集合本身的联合。例如,如果我们有一间戴着帽子的高个子房间,另一间戴着帽子的西班牙语房间,在合并这些房间后,我们唯一了解每个人的事情就是他们必须戴着帽子。
类型别名
我们一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但通常需要多次使用同一类型并通过单个名称来引用它。
类型别名正是如此 - 任何类型的名称。类型别名的语法是
tsTry
typePoint = {x : number;y : number;};// Exactly the same as the earlier examplefunctionprintCoord (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 });
实际上,你可以使用类型别名来命名任何类型,而不仅仅是对象类型。例如,类型别名可以命名联合类型
tsTry
typeID = number | string;
请注意,别名仅是别名 - 你不能使用类型别名来创建同一类型的不同/独立“版本”。当你使用别名时,它就像你写了别名类型一样。换句话说,这段代码可能看起来是非法的,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名
tsTry
typeUserInputSanitizedString = string;functionsanitizeInput (str : string):UserInputSanitizedString {returnsanitize (str );}// Create a sanitized inputletuserInput =sanitizeInput (getInput ());// Can still be re-assigned with a string thoughuserInput = "new input";
接口
接口声明是命名对象类型的另一种方式
tsTry
interfacePoint {x : number;y : number;}functionprintCoord (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 只关注我们传递给 printCoord
的值的结构 - 它只关心它具有预期的属性。只关注类型的结构和功能,这就是我们称 TypeScript 为结构化类型类型系统的原因。
类型别名和接口之间的差异
类型别名和接口非常相似,在许多情况下,你可以自由选择它们。interface
的几乎所有功能都可以在 type
中使用,关键的区别在于类型不能重新打开以添加新属性,而接口始终是可扩展的。
接口 |
类型 |
---|---|
扩展接口
|
通过交叉类型扩展类型
|
向现有接口添加新字段
|
创建后不能更改类型
|
你将在后面的章节中了解有关这些概念的更多信息,所以如果你现在还不能理解所有这些,请不要担心。
- 在 TypeScript 4.2 版本之前,类型别名名称可能出现在错误消息中,有时会代替等效的匿名类型(这可能是也可能不是理想的)。接口始终会在错误消息中命名。
- 类型别名可能不会参与声明合并,但接口可以。
- 接口只能用于声明对象的形状,而不是重命名基元。
- 接口名称始终以其原始形式出现在错误消息中,但仅在按名称使用它们时才会出现。
在大多数情况下,你可以根据个人喜好进行选择,并且 TypeScript 会告诉你是否需要将其作为另一种声明。如果你想要一个启发式方法,请使用 interface
,直到你需要使用 type
中的功能为止。