在本章中,我们将介绍一些在 JavaScript 代码中最常见的值的类型,并说明在 TypeScript 中描述这些类型相应的方法。 这不是一个详尽的列表,后续章节将描述命名和使用其他类型的更多方法。
类型还可以出现在许多 地方 ,而不仅仅是类型注释。 在我们了解类型本身的同时,我们还将了解在哪些地方可以引用这些类型来形成新的结构。
我们将首先回顾一下你在编写 JavaScript 或 TypeScript 代码时可能遇到的最基本和最常见的类型。 这些将在稍后形成更复杂类型的核心构建块。
基本类型:string
,number
,和 boolean
JavaScript has three very commonly used primitives: string
, number
, and boolean
.
Each has a corresponding type in TypeScript.
As you might expect, these are the same names you’d see if you used the JavaScript typeof
operator on a value of those types:
string
represents string values like"Hello, world"
number
is for numbers like42
. JavaScript does not have a special runtime value for integers, so there’s no equivalent toint
orfloat
- everything is simplynumber
boolean
is for the two valuestrue
andfalse
The type names
String
,Number
, andBoolean
(starting with capital letters) are legal, but refer to some special built-in types that will very rarely appear in your code. Always usestring
,number
, orboolean
for types.
Arrays
To specify the type of an array like [1, 2, 3]
, you can use the syntax number[]
; this syntax works for any type (e.g. string[]
is an array of strings, and so on).
You may also see this written as Array<number>
, which means the same thing.
We’ll learn more about the syntax T<U>
when we cover generics.
Note that
[number]
is a different thing; refer to the section on tuple types.
any
TypeScript also has a special type, any
, that you can use whenever you don’t want a particular value to cause typechecking errors.
When a value is of type any
, you can access any properties of it (which will in turn be of type any
), call it like a function, assign it to (or from) a value of any type, or pretty much anything else that’s syntactically legal:
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 ;
The any
type is useful when you don’t want to write out a long type just to convince TypeScript that a particular line of code is okay.
noImplicitAny
When you don’t specify a type, and TypeScript can’t infer it from context, the compiler will typically default to any
.
You usually want to avoid this, though, because any
isn’t type-checked.
Use the compiler flag noImplicitAny
to flag any implicit any
as an error.
Type Annotations on Variables
When you declare a variable using const
, var
, or let
, you can optionally add a type annotation to explicitly specify the type of the variable:
tsTry
letmyName : string = "Alice";
TypeScript doesn’t use “types on the left”-style declarations like
int x = 0;
Type annotations will always go after the thing being typed.
In most cases, though, this isn’t needed. Wherever possible, TypeScript tries to automatically infer the types in your code. For example, the type of a variable is inferred based on the type of its initializer:
tsTry
// No type annotation needed -- 'myName' inferred as type 'string'letmyName = "Alice";
For the most part you don’t need to explicitly learn the rules of inference. If you’re starting out, try using fewer type annotations than you think - you might be surprised how few you need for TypeScript to fully understand what’s going on.
Functions
Functions are the primary means of passing data around in JavaScript. TypeScript allows you to specify the types of both the input and output values of functions.
Parameter Type Annotations
When you declare a function, you can add type annotations after each parameter to declare what types of parameters the function accepts. Parameter type annotations go after the parameter name:
tsTry
// Parameter type annotationfunctiongreet (name : string) {console .log ("Hello, " +name .toUpperCase () + "!!");}
When a parameter has a type annotation, arguments to that function will be checked:
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 );
Even if you don’t have type annotations on your parameters, TypeScript will still check that you passed the right number of arguments.
Return Type Annotations
You can also add return type annotations. Return type annotations appear after the parameter list:
tsTry
functiongetFavoriteNumber (): number {return 26;}
Much like variable type annotations, you usually don’t need a return type annotation because TypeScript will infer the function’s return type based on its return
statements.
The type annotation in the above example doesn’t change anything.
Some codebases will explicitly specify a return type for documentation purposes, to prevent accidental changes, or just for personal preference.
Anonymous Functions
Anonymous functions are a little bit different from function declarations. When a function appears in a place where TypeScript can determine how it’s going to be called, the parameters of that function are automatically given types.
Here’s an example:
tsTry
// No type annotations here, but TypeScript can spot the bugconstnames = ["Alice", "Bob", "Eve"];// Contextual typing for functionnames .forEach (function (s ) {Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?2551Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?console .log (s .()); toUppercase });// Contextual typing also applies to arrow functionsnames .forEach ((s ) => {Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?2551Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?console .log (s .()); toUppercase });
Even though the parameter s
didn’t have a type annotation, TypeScript used the types of the forEach
function, along with the inferred type of the array, to determine the type s
will have.
This process is called contextual typing because the context that the function occurred in informed what type it should have. Similar to the inference rules, you don’t need to explicitly learn how this happens, but understanding that it does happen can help you notice when type annotations aren’t needed. Later, we’ll see more examples of how the context that a value occurs in can affect its type.
Object Types
Apart from primitives, the most common sort of type you’ll encounter is an object type. This refers to any JavaScript value with properties, which is almost all of them! To define an object type, we simply list its properties and their types.
For example, here’s a function that takes a point-like object:
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 });
Here, we annotated the parameter with a type with two properties - x
and y
- which are both of type number
.
You can use ,
or ;
to separate the properties, and the last separator is optional either way.
The type part of each property is also optional.
If you don’t specify a type, it will be assumed to be any
.
Optional Properties
Object types can also specify that some or all of their properties are optional.
To do this, add a ?
after the property name:
tsTry
functionprintName (obj : {first : string;last ?: string }) {// ...}// Both OKprintName ({first : "Bob" });printName ({first : "Alice",last : "Alisson" });
In JavaScript, if you access a property that doesn’t exist, you’ll get the value undefined
rather than a runtime error.
Because of this, when you read from an optional property, you’ll have to check for undefined
before using it.
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 ());}
Union Types
TypeScript’s type system allows you to build new types out of existing ones using a large variety of operators. Now that we know how to write a few types, it’s time to start combining them in interesting ways.
Defining a Union Type
The first way to combine types you might see is a union type. A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.
Let’s write a function that can operate on strings or numbers:
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 });
Working with Union Types
It’s easy to provide a value matching a union type - simply provide a type matching any of the union’s members. If you have a value of a union type, how do you work with it?
TypeScript will only allow you to do things with the union if that thing is valid for every member of the union.
For example, if you have the union string | number
, you can’t use methods that are only available on 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 }
The solution is to narrow the union with code, the same as you would in JavaScript without type annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the structure of the code.
For example, TypeScript knows that only a string
value will have a typeof
value "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 );}}
Another example is to use a function like 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 );}}
Notice that in the else
branch, we don’t need to do anything special - if x
wasn’t a string[]
, then it must have been a string
.
Sometimes you’ll have a union where all the members have something in common.
For example, both arrays and strings have a slice
method.
If every member in a union has a property in common, you can use that property without narrowing:
tsTry
// Return type is inferred as number[] | stringfunctiongetFirstThree (x : number[] | string) {returnx .slice (0, 3);}
It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union
number | string
is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
类型别名
我们通过直接在类型注解中编写对象类型和联合类型来使用它们。 这很方便,但是常常会想要多次使用同一个类型,并且通过一个名称引用它。
类型别名 正是如此 - 任意 类型 的一个 名称 。 类型别名的语法是:
tsTry
typePoint = {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 });
实际上,不只是对象类型,你可以使用类型别名为任何类型命名。 例如,类型别名可以命名联合类型:
tsTry
typeID = number | string;
请注意,别名 只是 别名 - 你不能使用类型别名创建同一类型的不同“版本”。 当你使用别名时,它与您编写的别名类型完全一样。 换句话说,这段代码 看起来 可能是非法的,但是对于 TypeScript 来说是正确的,因为这两种类型都是同一类型的别名:
tsTry
declare functiongetInput (): string;declare functionsanitize (str : string): string;// ---分割---typeUserInputSanitizedString = string;functionsanitizeInput (str : string):UserInputSanitizedString {returnsanitize (str );}// 创建一个经过清理的输入框letuserInput =sanitizeInput (getInput ());// 仍然可以使用字符串重新赋值userInput = "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
中使用,关键区别在于不能重新开放类型以添加新的属性,而接口始终是可扩展的。
Interface |
Type |
---|---|
扩展接口
|
通过 "&" 扩展类型
|
向现有接口添加新字段
|
类型创建后不能更改
|
在后面的章节中你会学到更多关于这些概念的知识,所以如果你没有立即理解这些知识,请不要担心。
- 在 TypeScript 4.2 之前,类型别名命名 可能 会出现在错误消息中,有时代替等效的匿名类型(可能需要也可能不需要)。接口在错误消息中将始终被命名。
- 类型别名不能参与 声明合并,但接口可以。
- 接口只能用于 声明对象的形状,不能重命名基本类型.
- 接口名称将 始终 以其原始形式出现 在错误消息中,但 只有 在按名称使用时才会出现。
在大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你它是否需要其他类型的声明。如果您想要启发式方法,可以使用 interface
直到你需要使用 type
中的功能。
Type Assertions
Sometimes you will have information about the type of a value that TypeScript can’t know about.
For example, if you’re using document.getElementById
, TypeScript only knows that this will return some kind of HTMLElement
, but you might know that your page will always have an HTMLCanvasElement
with a given ID.
In this situation, you can use a type assertion to specify a more specific type:
tsTry
constmyCanvas =document .getElementById ("main_canvas") asHTMLCanvasElement ;
Like a type annotation, type assertions are removed by the compiler and won’t affect the runtime behavior of your code.
You can also use the angle-bracket syntax (except if the code is in a .tsx
file), which is equivalent:
tsTry
constmyCanvas = <HTMLCanvasElement >document .getElementById ("main_canvas");
Reminder: Because type assertions are removed at compile-time, there is no runtime checking associated with a type assertion. There won’t be an exception or
null
generated if the type assertion is wrong.
TypeScript only allows type assertions which convert to a more specific or less specific version of a type. This rule prevents “impossible” coercions like:
tsTry
constConversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.2352Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.x = "hello" as number;
Sometimes this rule can be too conservative and will disallow more complex coercions that might be valid.
If this happens, you can use two assertions, first to any
(or unknown
, which we’ll introduce later), then to the desired type:
tsTry
consta =expr as any asT ;
Literal Types
In addition to the general types string
and number
, we can refer to specific strings and numbers in type positions.
One way to think about this is to consider how JavaScript comes with different ways to declare a variable. Both var
and let
allow for changing what is held inside the variable, and const
does not. This is reflected in how TypeScript creates types for literals.
tsTry
letchangingString = "Hello World";changingString = "Olá Mundo";// Because `changingString` can represent any possible string, that// is how TypeScript describes it in the type systemchangingString ;constconstantString = "Hello World";// Because `constantString` can only represent 1 possible string, it// has a literal type representationconstantString ;
By themselves, literal types aren’t very valuable:
tsTry
letx : "hello" = "hello";// OKx = "hello";// ...Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.= "howdy"; x
It’s not much use to have a variable that can only have one value!
But by combining literals into unions, you can express a much more useful concept - for example, functions that only accept a certain set of known values:
tsTry
functionprintText (s : string,alignment : "left" | "right" | "center") {// ...}printText ("Hello, world", "left");Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.2345Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.printText ("G'day, mate","centre" );
Numeric literal types work the same way:
tsTry
functioncompare (a : string,b : string): -1 | 0 | 1 {returna ===b ? 0 :a >b ? 1 : -1;}
Of course, you can combine these with non-literal types:
tsTry
interfaceOptions {width : number;}functionconfigure (x :Options | "auto") {// ...}configure ({width : 100 });configure ("auto");Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.2345Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.configure ("automatic" );
There’s one more kind of literal type: boolean literals.
There are only two boolean literal types, and as you might guess, they are the types true
and false
.
The type boolean
itself is actually just an alias for the union true | false
.
Literal Inference
When you initialize a variable with an object, TypeScript assumes that the properties of that object might change values later. For example, if you wrote code like this:
tsTry
constobj = {counter : 0 };if (someCondition ) {obj .counter = 1;}
TypeScript doesn’t assume the assignment of 1
to a field which previously had 0
is an error.
Another way of saying this is that obj.counter
must have the type number
, not 0
, because types are used to determine both reading and writing behavior.
The same applies to strings:
tsTry
constreq = {url : "https://example.com",method : "GET" };Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.2345Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.handleRequest (req .url ,req .method );
In the above example req.method
is inferred to be string
, not "GET"
. Because code can be evaluated between the creation of req
and the call of handleRequest
which could assign a new string like "GUESS"
to req.method
, TypeScript considers this code to have an error.
There are two ways to work around this.
-
You can change the inference by adding a type assertion in either location:
ts
Try// Change 1:constreq = {url : "https://example.com",method : "GET" as "GET" };// Change 2handleRequest (req .url ,req .method as "GET");Change 1 means “I intend for
req.method
to always have the literal type"GET"
”, preventing the possible assignment of"GUESS"
to that field after. Change 2 means “I know for other reasons thatreq.method
has the value"GET"
“. -
You can use
as const
to convert the entire object to be type literals:ts
Tryconstreq = {url : "https://example.com",method : "GET" } asconst ;handleRequest (req .url ,req .method );
The as const
suffix acts like const
but for the type system, ensuring that all properties are assigned the literal type instead of a more general version like string
or number
.
null
and undefined
JavaScript has two primitive values used to signal absent or uninitialized value: null
and undefined
.
TypeScript has two corresponding types by the same names. How these types behave depends on whether you have the strictNullChecks
option on.
strictNullChecks
off
With strictNullChecks
off, values that might be null
or undefined
can still be accessed normally, and the values null
and undefined
can be assigned to a property of any type.
This is similar to how languages without null checks (e.g. C#, Java) behave.
The lack of checking for these values tends to be a major source of bugs; we always recommend people turn strictNullChecks
on if it’s practical to do so in their codebase.
strictNullChecks
on
With strictNullChecks
on, when a value is null
or undefined
, you will need to test for those values before using methods or properties on that value.
Just like checking for undefined
before using an optional property, we can use narrowing to check for values that might be null
:
tsTry
functiondoSomething (x : string | null) {if (x === null) {// do nothing} else {console .log ("Hello, " +x .toUpperCase ());}}
Non-null Assertion Operator (Postfix !
)
TypeScript also has a special syntax for removing null
and undefined
from a type without doing any explicit checking.
Writing !
after any expression is effectively a type assertion that the value isn’t null
or undefined
:
tsTry
functionliveDangerously (x ?: number | null) {// No errorconsole .log (x !.toFixed ());}
Just like other type assertions, this doesn’t change the runtime behavior of your code, so it’s important to only use !
when you know that the value can’t be null
or undefined
.
Enums
Enums are a feature added to JavaScript by TypeScript which allows for describing a value which could be one of a set of possible named constants. Unlike most TypeScript features, this is not a type-level addition to JavaScript but something added to the language and runtime. Because of this, it’s a feature which you should know exists, but maybe hold off on using unless you are sure. You can read more about enums in the Enum reference page.
Less Common Primitives
It’s worth mentioning the rest of the primitives in JavaScript which are represented in the type system. Though we will not go into depth here.
bigint
From ES2020 onwards, there is a primitive in JavaScript used for very large integers, BigInt
:
tsTry
// Creating a bigint via the BigInt functionconstoneHundred : bigint =BigInt (100);// Creating a BigInt via the literal syntaxconstanotherHundred : bigint = 100n;
You can learn more about BigInt in the TypeScript 3.2 release notes.
symbol
There is a primitive in JavaScript used to create a globally unique reference via the function Symbol()
:
tsTry
constfirstName =Symbol ("name");constsecondName =Symbol ("name");if (This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.2367This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.firstName ===secondName ) {// Can't ever happen}
You can learn more about them in Symbols reference page.