In this chapter, we’ll cover some of the most common types of values you’ll find in JavaScript code, and explain the corresponding ways to describe those types in TypeScript. This isn’t an exhaustive list, and future chapters will describe more ways to name and use other types.
在本章中,我们将介绍一些在JavaScript代码中最常见的值类型,并解释在TypeScript中描述这些类型的相应方法。这不是一个详尽的列表,未来的章节将描述更多的方法来命名和使用其他类型。
Types can also appear in many more places than just type annotations. As we learn about the types themselves, we’ll also learn about the places where we can refer to these types to form new constructs.
除了类型注解之外,类型还可以出现在更多地方。当我们了解类型本身的时候,我们也会了解我们可以在什么地方引用这些类型来构建新的概念。
We’ll start by reviewing the most basic and common types you might encounter when writing JavaScript or TypeScript code. These will later form the core building blocks of more complex types.
首先,我们要回顾一下你在编写JavaScript或TypeScript代码时可能遇到的最基本和最常见的类型。这些稍后将构成更复杂类型的核心模块。
The primitives: string, number, and 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:
Javascript有三个非常常用的基础类型:string, number, 和boolean。它们在TypeScript中都有对应的类型。如你所料,如果你对这些类型的值使用Javascript的typeof操作符,你会看到这些名字:
stringrepresents string values like"Hello, world"numberis for numbers like42. JavaScript does not have a special runtime value for integers, so there’s no equivalent tointorfloat- everything is simplynumbernumber是42这样的数字。Javascript没有针对整数的特殊运行时值,因此没有与int或float等价的东西——所有东西都只是数字booleanis for the two valuestrueandfalse
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, orbooleanfor types. 类型名称String、Number和Boolean(以大写字母开始)是合法的,但这些指的是一些在代码中很少出现的特殊内置类型。大多数使用时都是用小写的string,number,boolean。
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.
要指定[1,2,31]这样的数组类型,可以使用语法number[];该语法适用于任何类型(例如string[]是字符串数组等等)。你也可以把它写成Array<number>,意思是一样的。当我们讨论泛型时,我们会学到更多关于语法T的知识。
Note that
[number]is a different thing; refer to the section on tuple types. 注意[number]是另一回事;请参阅元组类型一节。
any
TypeScript also has a special type, any, that you can use whenever you don’t want a particular value to cause typechecking errors.
TypeScript还有一个特殊的类型any,当你不想让某个特定的值导致类型检查错误时,你可以使用它。
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:
当一个值是any类型时,你可以访问它的任何属性(这将是any类型),像函数一样调用它,将它赋值给任何类型的值,或者几乎任何语法上合法的东西:
let obj: any = { x: 0 };// 以下都是正确的obj.foo();obj();obj.bar = 100;obj = "hello";const n: 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.
当你不想为了让TypeScript相信某一行代码是合适的而写一个长类型时,any类型是很有用的。
noImplicitAny
没有隐式any
When you don’t specify a type, and Typescript can’t infer it from context, the compiler will typically default to any.
当你没有指定一个类型,并且Typescript不能从上下文推断它时,编译器通常会默认为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.
不过,你通常希望避免这种情况,因为any没有进行类型检查。使用编译器标记noImplicitAny来将任何隐式的any标记为错误。
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:
当你使用const、var或let声明变量时,你可以选择添加类型注解来显式指定变量的类型!
let myName: 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. TypeScript不使用像int x = 0这样的“左侧类型”类型声明;类型注解总是在输入的变量后面。
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:
但是,在大多数情况下,这是不需要的。只要可能,Typescript会自动推断代码中的类型。例如,变量的类型是根据其初始化式的类型来推断的:
// 不需要类型注解-'myName'推断为类型'sting'let myName = "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.
在大多数情况下,您不需要明确地学习推理规则。如果你刚开始,试着使用比你想象中更少的类型注解——你可能会惊讶地发现,Typescript只需要这么少的代码就能完全理解到底发生了什么。
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.
函数是JavaScript中传递数据的主要方式。TypeScript允许你指定函数的输入和输出值的类型。
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:
在声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注解位于参数名之后:
// Parameter type annotationfunction greet(name: string) {console.log("Hello, " + name.toUpperCase() + "!!");}
When a parameter has a type annotations, arguments to that function will be checked:
当形参有类型注解时,将检查该函数的实参:
// Would be a runtime error if executed!greet(42);Argument of type 'number' is not assignable to parameter of type 'string'.
Even if you don’t have type annotations on your parameters, TypeScript will still check that you passed the right number of arguments. 即使你的参数上没有类型注释,TypeScript仍然会检查你是否传递了正确的参数数量。
Return Type Annotations
返回值类型注解
You can also add return type annotations. Return type annotations appear after the parameter list:
还可以添加返回值类型注解。返回值类型注解出现在参数列表后面:
function getFavoriteNumber(): 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.
很像变量类型注解,你通常不需要返回值类型注解,因为TypeScript会根据函数的返回语句推断函数的返回类型。上面例子中的类型注解没有改变任何东西。一些代码库将显式地指定返回类型,用于文档用途、防止意外更改或仅用于个人偏好。
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.
匿名函数与函数声明有一点不同。当一个函数出现在TypeScript可以决定它如何被调用的地方时,该函数的参数就会自动指定类型
Here’s an example:
有个例子:
const names = ["Alice", "Bob", "Eve"];//这里没有类型注解,但TypeScript可以发现这个错误names.forEach(function (s) {console.log(s.toUppercase());Property 'toUppercase' does not exist on type 'string'. Did you mean '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.
尽管形参s没有类型注解,但TypeScript使用forEach函数的类型,以及推断出的数组类型,来确定s将拥有的类型。
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:
// The parameter's type annotation is an object typefunction 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 });
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:
function printName(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.
function printName(obj: { first: string; last?: string }) {// Error - might crash if 'obj.last' wasn't provided!console.log(obj.last.toUpperCase());Object is possibly 'undefined'.2532Object is possibly 'undefined'.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 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:
function printId(id: number | string) {console.log("Your ID is: " + id);}// OKprintId(101);// OKprintId("202");// ErrorprintId({ myID: 22342 });Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.Type '{ myID: number; }' is not assignable to type 'number'.2345Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.Type '{ myID: number; }' is not assignable to type 'number'.
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:
function printId(id: number | string) {console.log(id.toUpperCase());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'.}
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":
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);}}
Another example is to use a function like 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);}}
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:
// Return type is inferred as number[] | stringfunction getFirstThree(x: number[] | string) {return x.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 | stringis 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 wearings hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
Type Aliases
We’ve been using object types and union types by writing them directly in type annotations. This is convenient, but it’s common to want to use the same type more than once and refer to it by a single name.
A type alias is exactly that - a name for any type. The syntax for a type alias is:
type Point = {x: number;y: number;};// Exactly the same as the earlier examplefunction 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 });
You can actually use a type alias to give a name to any type at all, not just an object type. For example, a type alias can name a union type:
type ID = number | string;
Note that aliases are only aliases - you cannot use type aliases to create different/distinct “versions” of the same type. When you use the alias, it’s exactly as if you had written the aliased type. In other words, this code might look illegal, but is OK according to TypeScript because both types are aliases for the same type:
type UserInputSanitizedString = string;function sanitizeInput(str: string): UserInputSanitizedString {return sanitize(str);}// Create a sanitized inputlet userInput = sanitizeInput(getInput());// Can still be re-assigned with a string thoughuserInput = "new input";
Interfaces
An interface declaration is another way to name an object type:
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 });
Just like when we used a type alias above, the example works just as if we had used an anonymous object type. TypeScript is only concerned with the structure of the value we passed to printCoord - it only cares that it has the expected properties. Being concerned only with the structure and capabilities of types is why we call TypeScript a structurally typed type system.
Differences Between Type Aliases and Interfaces
Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.
Interface |
Type |
|---|---|
| Extending an interface interface Bear extends Animal { |
honey: boolean
}
const bear = getBear()
bear.name
bear.honey` | Extending a type via intersections<br />
|
| Adding new fields to an existing interface
| A type cannot be changed after being created
` |
You’ll learn more about these concepts in later chapters, so don’t worry if you don’t understand all of these right away.
- Prior to TypeScript version 4.2, type alias names may appear in error messages, sometimes in place of the equivalent anonymous type (which may or may not be desirable). Interfaces will always be named in error messages.
- Type aliases may not participate in declaration merging, but interfaces can.
- Interfaces may only be used to declare the shapes of object, not re-name primitives.
- Interface names will always appear in their original form in error messages, but only when they are used by name.
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from 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:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
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:
const myCanvas = <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
nullgenerated 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:
const x = "hello" as number;Conversion 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.
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:
const a = (expr as any) as T;
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.
let changingString = "Hello World";changingString = "Ola Mundo";// Because `changingString` can represent any possible string, that// is how TypeScript describes it in the type systemchangingString;// ^ = let changingString: stringconst constantString = "Hello World";// Because `constantString` can only represent 1 possible string, it// has a literal type representationconstantString;// ^ = const constantString: "Hello World"
By themselves, literal types aren’t very valuable:
let x: "hello" = "hello";// OKx = "hello";// ...x = "howdy";Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.
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:
function printText(s: string, alignment: "left" | "right" | "center") {// ...}printText("Hello, world", "left");printText("G'day, mate", "centre");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"'.
Numeric literal types work the same way:
function compare(a: string, b: string): -1 | 0 | 1 {return a === b ? 0 : a > b ? 1 : -1;}
Of course, you can combine these with non-literal types:
interface Options {width: number;}function configure(x: Options | "auto") {// ...}configure({ width: 100 });configure("auto");configure("automatic");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"'.
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:
const obj = { 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:
const req = { url: "https://example.com", method: "GET" };handleRequest(req.url, req.method);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"'.
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:
// Change 1:const req = { url: "https://example.com", method: "GET" as "GET" };// Change 2handleRequest(req.url, req.method as "GET");
Change 1 means “I intend for
req.methodto 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.methodhas the value"GET"“.- You can use
as constto convert the entire object to be type literals:const req = { url: "https://example.com", method: "GET" } as const;handleRequest(req.url, req.method);
The as const prefix 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:
function doSomething(x: string | undefined) {if (x === undefined) {// 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:
function liveDangerously(x?: number | undefined) {// 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:
// Creating a bigint via the BigInt functionconst oneHundred: bigint = BigInt(100);// Creating a BigInt via the literal syntaxconst anotherHundred: 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():
const firstName = Symbol("name");const secondName = Symbol("name");if (firstName === secondName) {This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.2367This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.// Can't ever happen}
You can learn more about them in Symbols reference page.
