Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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.
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.
string
,number
, andboolean
JavaScript has three very commonly used [primitives](https: //developer.mozilla.org/en-US/docs/Glossary/Primitive): 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 like 42
. JavaScript does not have a special runtime value for integers, so there's no equivalent to int
or float
- everything is simply number
boolean
is for the two values true
and false
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.
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 [Tuples](https: //www.typescriptlang.org/docs/handbook/2/objects.html#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:
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
](https: //www.typescriptlang.org/tsconfig#noImplicitAny) to flag any implicit any
as an error.
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:
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:
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 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.
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:
When a parameter has a type annotation, arguments to that function will be checked:
Even if you don't have type annotations on your parameters, TypeScript will still check that you passed the right number of arguments.
You can also add return type annotations. Return type annotations appear after the parameter list:
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 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:
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 within informs 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.
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:
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
.
Object types can also specify that some or all of their properties are optional. To do this, add a ?
after the property name:
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.
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.
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:
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 an operation if it 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
:
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"
:
Another example is to use a function like Array.isArray
:
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:
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.
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:
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:
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:
An interface declaration is another way to name an object type:
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.
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.
|
Extending an interface
|
Extending a type via intersections
| |
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](https: //www.typescriptlang.org/play?#code/PTAEGEHsFsAcEsA2BTATqNrLusgzngIYDm+oA7koqIYuYQJ56gCueyoAUCKAC4AWHAHaFcoSADMaQ0PCG80EwgGNkALk6c5C1EtWgAsqOi1QAb06groEbjWg8vVHOKcAvpokshy3vEgyyMr8kEbQJogAFND2YREAlOaW1soBeJAoAHSIkMTRmbbI8e6aPMiZxJmgACqCGKhY6ABGyDnkFFQ0dIzMbBwCwqIccabcYLyQoKjIEmh8kwN8DLAc5PzwwbLMyAAeK77IACYaQSEjUWZWhfYAjABMAMwALA+gbsVjoADqgjKESytQPxCHghAByXigYgBfr8LAsYj8aQMUASbDQcRSExCeCwFiIQh+AKfAYyBiQFgOPyIaikSGLQo0Zj-aazaY+dSaXjLDgAGXgAC9CKhDqAALxJaw2Ib2RzOISuDycLw+ImBYKQflCkWRRD2LXCw6JCxS1JCdJZHJ5RAFIbFJU8ADKC3WzEcnVZaGYE1ABpFnFOmsFhsil2uoHuzwArO9SmAAEIsSFrZB-GgAjjA5gtVN8VCEc1o1C4Q4AGlR2AwO1EsBQoAAbvB-gJ4HhPgB5aDwem-Ph1TCV3AEEirTp4ELtRbTPD4vwKjOfAuioSQHuDXBcnmgACC+eCONFEs73YAPGGZVT5cRyyhiHh7AAON7lsG3vBggB8XGV3l8-nVISOgghxoLq9i7io-AHsayRWGaFrlFauq2rg9qaIGQHwCBqChtKdgRo8TxRjeyB3o+7xAA), 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](https: //www.typescriptlang.org/play?#code/PTAEEEDtQS0gXApgJwGYEMDGjSfdAIx2UQFoB7AB0UkQBMAoEUfO0Wgd1ADd0AbAK6IAzizp16ALgYM4SNFhwBZdAFtV-UAG8GoPaADmNAcMmhh8ZHAMMAvjLkoM2UCvWad+0ARL0A-GYWVpA29gyY5JAWLJAwGnxmbvGgALzauvpGkCZmAEQAjABMAMwALLkANBl6zABi6DB8okR4Jjg+iPSgABboovDk3jjo5pbW1d6+dGb5djLwAJ7UoABKiJTwjThpnpnGpqPBoTLMAJrkArj4kOTwYmycPOhW6AR8IrDQ8N04wmo4HHQCwYi2Waw2W1S6S8HX8gTGITsQA).
Interfaces may only be used to [declare the shapes of objects, not rename primitives](https: //www.typescriptlang.org/play?#code/PTAEAkFMCdIcgM6gC4HcD2pIA8CGBbABwBtIl0AzUAKBFAFcEBLAOwHMUBPQs0XFgCahWyGBVwBjMrTDJMAshOhMARpD4tQ6FQCtIE5DWoixk9QEEWAeV37kARlABvaqDegAbrmL1IALlAEZGV2agBfampkbgtrWwMAJlAAXmdXdy8ff0Dg1jZwyLoAVWZ2Lh5QVHUJflAlSFxROsY5fFAWAmk6CnRoLGwmILzQQmV8JmQmDzI-SOiKgGV+CaYAL0gBBdyy1KCQ-Pn1AFFplgA5enw1PtSWS+vCsAAVAAtB4QQWOEMKBuYVUiVCYvYQsUTQcRSBDGMGmKSgAAa-VEgiQe2GLgKQA).
Interface names will [always appear in their original form](https: //www.typescriptlang.org/play?#code/PTAEGEHsFsAcEsA2BTATqNrLusgzngIYDm+oA7koqIYuYQJ56gCueyoAUCKAC4AWHAHaFcoSADMaQ0PCG80EwgGNkALk6c5C1EtWgAsqOi1QAb06groEbjWg8vVHOKcAvpokshy3vEgyyMr8kEbQJogAFND2YREAlOaW1soBeJAoAHSIkMTRmbbI8e6aPMiZxJmgACqCGKhY6ABGyDnkFFQ0dIzMbBwCwqIccabcYLyQoKjIEmh8kwN8DLAc5PzwwbLMyAAeK77IACYaQSEjUWY2Q-YAjABMAMwALA+gbsVjNXW8yxySoAADaAA0CCaZbPh1XYqXgOIY0ZgmcK0AA0nyaLFhhGY8F4AHJmEJILCWsgZId4NNfIgGFdcIcUTVfgBlZTOWC8T7kAJ42G4eT+GS42QyRaYbCgXAEEguTzeXyCjDBSAAQSE8Ai0Xsl0K9kcziExDeiQs1lAqSE6SyOTy0AKQ2KHk4p1V6s1OuuoHuzwArMagA) 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
.
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:
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:
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:
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:
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.
By themselves, literal types aren't very valuable:
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:
Numeric literal types work the same way:
Of course, you can combine these with non-literal types:
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
.
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:
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:
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");Try
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
andundefined
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
](https: //www.typescriptlang.org/tsconfig#strictNullChecks) option on.
strictNullChecks
offWith [strictNullChecks
](https: //www.typescriptlang.org/tsconfig#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
](https: //www.typescriptlang.org/tsconfig#strictNullChecks) on if it's practical to do so in their codebase.
strictNullChecks
onWith [strictNullChecks
](https: //www.typescriptlang.org/tsconfig#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
:
!
)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
:
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 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](https: //www.typescriptlang.org/docs/handbook/enums.html).
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
:
You can learn more about BigInt in [the TypeScript 3.2 release notes](https: //www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#bigint).
symbol
There is a primitive in JavaScript used to create a globally unique reference via the function Symbol()
:
You can learn more about them in [Symbols reference page](https: //www.typescriptlang.org/docs/handbook/symbols.html).
Interface
Type
Excerpt
It is very simple to get started with TypeScript, but sometimes we need to think more about the best use case for us. In this case, types or interfaces?
The idea of having static type-checking in JavaScript is really fantastic and the adoption of TypeScript is growing more every day.
You started to use TypeScript in your project, you created your first type, then you jumped to your first interface, and you got it working. You concluded that TypeScript, in fact, was helping your development and saving you precious time, but you might have made some mistakes and not followed the best practices when you started to work with types and interfaces in TypeScript.
This is the case for a lot of developers, they don't really know the real difference between type aliases and interfaces in TypeScript.
It is very simple to get started with TypeScript, but sometimes we need to think more about the best use case for us. In this case, types or interfaces?
Before we jump into the differences between types and interfaces in TypeScript, we need to understand something.
In TypeScript, we have a lot of basic types, such as string, boolean, and number. These are the basic types of TypeScript. You can check the list of all the basic types [here](https: //www.typescriptlang.org/docs/handbook/basic-types.html#table-of-contents). Also, in TypeScript, we have advanced types and in these [advanced types](https: //www.typescriptlang.org/docs/handbook/advanced-types.html), we have something called [type aliases](https: //www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases). With type aliases, we can create a new name for a type but we don't define a new type.
We use the type
keyword to create a new type alias, that's why some people might get confused and think that it's creating a new type when they're only creating a new name for a type. So, when you hear someone talking about the differences between types and interfaces, like in this article, you can assume that this person is talking about type aliases vs interfaces.
We will use the [TypeScript Playground](https: //www.typescriptlang.org/play/index.html#) for code examples. The [TypeScript Playground](https: //www.typescriptlang.org/play/index.html#) allows us to work and test the latest version of TypeScript (or any other version we want to), and we will save time by using this playground rather than creating a new TypeScript project just for examples.
The difference between types and interfaces in TypeScript used to be more clear, but with the latest versions of TypeScript, they're becoming more similar.
Interfaces are basically a way to describe data shapes, for example, an object.
Type is a definition of a type of data, for example, a union, primitive, intersection, tuple, or any other type.
One thing that's possible to do with interfaces but are not with types is declaration merging. Declaration merging happens when the TypeScript compiler merges two or more interfaces that share the same name into only one declaration.
Let's imagine that we have two interfaces called Song
, with different properties:
interface Song { artistName: string; }; interface Song { songName: string; }; const song: Song = { artistName: "Freddie", songName: "The Chain" };
TypeScript will automatically merge both interfaces declarations into one, so when we use this Song
interface, we'll have both properties.
Declaration merging does not work with types. If we try to create two types with the same names, but with different properties, TypeScript would still throw us an error.
Duplicate identifier Song.
In TypeScript, we can easily extend and implement interfaces. This is not possible with types though.
Interfaces in TypeScript can extend classes, this is a very awesome concept that helps a lot in a more object-oriented way of programming. We can also create classes implementing interfaces.
For example, let's imagine that we have a class called Car
and an interface called NewCar
, we can easily extend this class using an interface:
class Car { printCar = () => { console.log("this is my car") } }; interface NewCar extends Car { name: string; }; class NewestCar implements NewCar { name: "Car"; constructor(engine:string) { this.name = name } printCar = () => { console.log("this is my car") } };
Intersection allows us to combine multiple types into a single one type. To create an intersection type, we have to use the &
keyword:
type Name = { name: "string" }; type Age = { age: number }; type Person = Name & Age;
The nice thing here is that we can create a new intersection type combining two interfaces, for example, but not the other way around. We cannot create an interface combining two types, because it doesn't work:
interface Name { name: "string" }; interface Age { age: number }; type Person = Name & Age;
Union types allow us to create a new type that can have a value of one or a few more types. To create a union type, we have to use the |
keyword.
type Man = { name: "string" }; type Woman = { name: "string" }; type Person = Man | Woman;
Similar to intersections, we can create a new union type combining two interfaces, for example, but not the other way around:
interface Man { name: "string" }; interface Woman { name: "string" }; type Person = Man | Woman;
[Tuples](https: //www.typescriptlang.org/docs/handbook/basic-types.html#tuple) are a very helpful concept in TypeScript, it brought to us this new data type that includes two sets of values of different data types.
type Reponse = [string, number]
But, in TypeScript, we can only declare tuples using types and not interfaces. There's no way we can declare a tuple in TypeScript using an interface, but you still are able to use a tuple inside an interface, like this:
interface Response { value: [string, number] }
We can see that we can achieve the same result as using types with interfaces. So, here comes the question that a lot of developers might have — should I use a type instead of an interface? If so, when should I use a type?
Let's understand the best use cases for both of them, so you don't need to abandon one for the other.
This question is really tricky, and the answer to it, you might guess, depends on what you're building and what you're working on.
Interfaces are better when you need to define a new object or method of an object. For example, in React applications, when you need to define the props that a specific component is going to receive, it's ideal to use interface over types:
interface TodoProps { name: string; isCompleted: boolean }; const Todo: React.FC<TodoProps> = ({ name, isCompleted }) => { ... };
Types are better when you need to create functions, for example. Let's imagine that we have a function that's going to return an object called, type alias is more recommended for this approach:
type Person = { name: string, age: number }; type ReturnPerson = ( person: Person ) => Person; const returnPerson: ReturnPerson = (person) => { return person; };
At the end of the day, to decide if you should use a type alias or an interface, you should carefully think and analyze the situation — what you're working on, the specific code, etc.
Interface work better with objects and method objects, and types are better to work with functions, complex types, etc.
You should not start to use one and delete the other. Instead of doing that, start to refactor slowly, thinking of what makes more sense to that specific situation.
Remember that you can use both together and they will work fine. The idea here is just to clarify the differences between types and interfaces, and the best use cases for both.
In this article, we learned more about the differences between types and interfaces in TypeScript. We learned that type aliases are advanced types in TypeScript, we learned the best use cases for both types and interfaces in TypeScript, and how we can apply both of them in real projects.
Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.
Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.
We'll first start off with numeric enums, which are probably more familiar if you're coming from other languages. An enum can be defined using the enum
keyword.
Above, we have a numeric enum where Up
is initialized with 1
. All of the following members are auto-incremented from that point on. In other words, Direction.Up
has the value 1
, Down
has 2
, Left
has 3
, and Right
has 4
.
If we wanted, we could leave off the initializers entirely:
Here, Up
would have the value 0
, Down
would have 1
, etc. This auto-incrementing behavior is useful for cases where we might not care about the member values themselves, but do care that each value is distinct from other values in the same enum.
Using an enum is simple: just access any member as a property off of the enum itself, and declare types using the name of the enum:
Numeric enums can be mixed in [computed and constant members (see below)](https: //www.typescriptlang.org/docs/handbook/enums.html#computed-and- constant-members). The short story is, enums without initializers either need to be first, or have to come after numeric enums initialized with numeric constants or other constant enum members. In other words, the following isn't allowed:
String enums are a similar concept, but have some subtle [runtime differences](https: //www.typescriptlang.org/docs/handbook/enums.html#enums-at-runtime) as documented below. In a string enum, each member has to be constant-initialized with a string literal, or with another string enum member.
While string enums don't have auto-incrementing behavior, string enums have the benefit that they "serialize" well. In other words, if you were debugging and had to read the runtime value of a numeric enum, the value is often opaque - it doesn't convey any useful meaning on its own (though [reverse mapping](https: //www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings) can often help). String enums allow you to give a meaningful and readable value when your code runs, independent of the name of the enum member itself.
Technically enums can be mixed with string and numeric members, but it's not clear why you would ever want to do so:
Unless you're really trying to take advantage of JavaScript's runtime behavior in a clever way, it's advised that you don't do this.
constant members
Each enum member has a value associated with it which can be either _ constant_ or computed. An enum member is considered constant if:
It is the first member in the enum and it has no initializer, in which case it's assigned the value 0
:
The enum member is initialized with a constant enum expression. A constant enum expression is a subset of TypeScript expressions that can be fully evaluated at compile time. An expression is a constant enum expression if it is:
a literal enum expression (basically a string literal or a numeric literal)
a reference to previously defined constant enum member (which can originate from a different enum)
a parenthesized constant enum expression
one of the +
, -
, ~
unary operators applied to constant enum expression
+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
binary operators with constant enum expressions as operands It is a compile time error for constant enum expressions to be evaluated to NaN
or Infinity
.
In all other cases enum member is considered computed.
There is a special subset of constant enum members that aren't calculated: literal enum members. A literal enum member is a constant enum member with no initialized value, or with values that are initialized to
any string literal (e.g. "foo"
, "bar
, "baz"
)
any numeric literal (e.g. 1
, 100
)
a unary minus applied to any numeric literal (e.g. -1
, -100
)
When all members in an enum have literal enum values, some special semantics come into play.
The first is that enum members also become types as well! For example, we can say that certain members can only have the value of an enum member:
The other change is that enum types themselves effectively become a union of each enum member. With union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Because of that, TypeScript can catch bugs where we might be comparing values incorrectly. For example:
In that example, we first checked whether x
was not E.Foo
. If that check succeeds, then our ||
will short-circuit, and the body of the ‘if' will run. However, if the check didn't succeed, then x
can only be E.Foo
, so it doesn't make sense to see whether it's equal to E.Bar
.
Enums are real objects that exist at runtime. For example, the following enum
can actually be passed around to functions
Even though Enums are real objects that exist at runtime, the keyof
keyword works differently than you might expect for typical objects. Instead, use keyof typeof
to get a Type that represents all Enum keys as strings.
In addition to creating an object with property names for members, numeric enums members also get a reverse mapping from enum values to enum names. For example, in this example:
TypeScript compiles this down to the following JavaScript:
In this generated code, an enum is compiled into an object that stores both forward ( name
-> value
) and reverse ( value
-> name
) mappings. References to other enum members are always emitted as property accesses and never inlined.
Keep in mind that string enum members do not get a reverse mapping generated at all.
const` enums
In most cases, enums are a perfectly valid solution. However sometimes requirements are tighter. To avoid paying the cost of extra generated code and additional indirection when accessing enum values, it's possible to use const
enums. const enums are defined using the const
modifier on our enums:
const enums can only use constant enum expressions and unlike regular enums they are completely removed during compilation. const enum members are inlined at use sites. This is possible since const enums cannot have computed members.
in generated code will become
** const enum pitfalls**
Inlining enum values is straightforward at first, but comes with subtle implications. These pitfalls pertain to ambient const enums only (basically const enums in .d.ts
files) and sharing them between projects, but if you are publishing or consuming .d.ts
files, these pitfalls likely apply to you, because tsc --declaration
transforms .ts
files into .d.ts
files.
For the reasons laid out in the [isolatedModules
documentation](https: //www.typescriptlang.org/tsconfig#references-to- const-enum-members), that mode is fundamentally incompatible with ambient const enums. This means if you publish ambient const enums, downstream consumers will not be able to use [isolatedModules
](https: //www.typescriptlang.org/tsconfig#isolatedModules) and those enum values at the same time.
You can easily inline values from version A of a dependency at compile time, and import version B at runtime. Version A and B's enums can have different values, if you are not very careful, resulting in [surprising bugs](https: //github.com/microsoft/TypeScript/issues/5219#issue-110947903), like taking the wrong branches of if
statements. These bugs are especially pernicious because it is common to run automated tests at roughly the same time as projects are built, with the same dependency versions, which misses these bugs completely.
[importsNotUsedAsValues: "preserve"
](https: //www.typescriptlang.org/tsconfig#importsNotUsedAsValues) will not elide imports for const enums used as values, but ambient const enums do not guarantee that runtime .js
files exist. The unresolvable imports cause errors at runtime. The usual way to unambiguously elide imports, [type-only imports](https: //www.typescriptlang.org/docs/handbook/modules.html#importing-types), [does not allow const enum values](https: //github.com/microsoft/TypeScript/issues/40344), currently.
Here are two approaches to avoiding these pitfalls:
A. Do not use const enums at all. You can easily [ban const enums](https: //github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#how-can-i-ban-specific-language-feature) with the help of a linter. Obviously this avoids any issues with const enums, but prevents your project from inlining its own enums. Unlike inlining enums from other projects, inlining a project's own enums is not problematic and has performance implications. B. Do not publish ambient const enums, by de constifying them with the help of [preserve constEnums
](https: //www.typescriptlang.org/tsconfig#preserve constEnums). This is the approach taken internally by the [TypeScript project itself](https: //github.com/microsoft/TypeScript/pull/5422). [preserve constEnums
](https: //www.typescriptlang.org/tsconfig#preserve constEnums)emits the same JavaScript for const enums as plain enums. You can then safely strip the const
modifier from .d.ts
files [in a build step](https: //github.com/microsoft/TypeScript/blob/1a981d1df1810c868a66b3828497f049a944951c/Gulpfile.js#L144).
This way downstream consumers will not inline enums from your project, avoiding the pitfalls above, but a project can still inline its own enums, unlike banning const enums entirely.
Ambient enums are used to describe the shape of already existing enum types.
One important difference between ambient and non-ambient enums is that, in regular enums, members that don't have an initializer will be considered constant if its preceding enum member is considered constant. By contrast, an ambient (and non- const) enum member that does not have an initializer is always considered computed.
In modern TypeScript, you may not need an enum when an object with as const
could suffice:
\
In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types.
As we've seen, they can be anonymous:
or they can be named by using either an interface
or a type alias.
In all three examples above, we've written functions that take objects that contain the property name
(which must be a string
) and age
(which must be a number
).
Each property in an object type can specify a couple of things: the type, whether the property is optional, and whether the property can be written to.
Much of the time, we'll find ourselves dealing with objects that might have a property set. In those cases, we can mark those properties as optional by adding a question mark (?
) to the end of their names.
In this example, both xPos
and yPos
are considered optional. We can choose to provide either of them, so every call above to paintShape
is valid. All optionality really says is that if the property is set, it better have a specific type.
We can also read from those properties - but when we do under [strictNullChecks
](https: //www.typescriptlang.org/tsconfig#strictNullChecks), TypeScript will tell us they're potentially undefined
.
In JavaScript, even if the property has never been set, we can still access it - it's just going to give us the value undefined
. We can just handle undefined
specially.
Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to support it.
Here we used [a destructuring pattern](https: //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) for paintShape
's parameter, and provided [default values](https: //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Default_values) for xPos
and yPos
. Now xPos
and yPos
are both definitely present within the body of paintShape
, but optional for any callers to paintShape
.
Note that there is currently no way to place type annotations within destructuring patterns. This is because the following syntax already means something different in JavaScript.
In an object destructuring pattern, shape: Shape
means "grab the property shape
and redefine it locally as a variable named Shape
. Likewise xPos: number
creates a variable named number
whose value is based on the parameter's xPos
.
Using [mapping modifiers](https: //www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers), you can remove optional
attributes.
readonly
PropertiesProperties can also be marked as readonly
for TypeScript. While it won't change any behavior at runtime, a property marked as readonly
can't be written to during type-checking.
Using the readonly
modifier doesn't necessarily imply that a value is totally immutable - or in other words, that its internal contents can't be changed. It just means the property itself can't be re-written to.
It's important to manage expectations of what readonly
implies. It's useful to signal intent during development time for TypeScript on how an object should be used. TypeScript doesn't factor in whether properties on two types are readonly
when checking whether those types are compatible, so readonly
properties can also change via aliasing.
Using [mapping modifiers](https: //www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers), you can remove readonly
attributes.
Sometimes you don't know all the names of a type's properties ahead of time, but you do know the shape of the values.
In those cases you can use an index signature to describe the types of possible values, for example:
Above, we have a StringArray
interface which has an index signature. This index signature states that when a StringArray
is indexed with a number
, it will return a string
.
An index signature property type must be either ‘string' or ‘number'.
While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return type. This is because a string index declares that obj.property
is also available as obj["property"]
. In the following example, name
's type does not match the string index's type, and the type checker gives an error:
However, properties of different types are acceptable if the index signature is a union of the property types:
Finally, you can make index signatures readonly
in order to prevent assignment to their indices:
You can't set myArray[2]
because the index signature is readonly
.
It's pretty common to have types that might be more specific versions of other types. For example, we might have a BasicAddress
type that describes the fields necessary for sending letters and packages in the U.S.
In some situations that's enough, but addresses often have a unit number associated with them if the building at an address has multiple units. We can then describe an AddressWithUnit
.
This does the job, but the downside here is that we had to repeat all the other fields from BasicAddress
when our changes were purely additive. Instead, we can extend the original BasicAddress
type and just add the new fields that are unique to AddressWithUnit
.
The extends
keyword on an interface
allows us to effectively copy members from other named types, and add whatever new members we want. This can be useful for cutting down the amount of type declaration boilerplate we have to write, and for signaling intent that several different declarations of the same property might be related. For example, AddressWithUnit
didn't need to repeat the street
property, and because street
originates from BasicAddress
, a reader will know that those two types are related in some way.
interface
s can also extend from multiple types.
interface
s allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types.
An intersection type is defined using the &
operator.
Here, we've intersected Colorful
and Circle
to produce a new type that has all the members of Colorful
and Circle
.
We just looked at two ways to combine types which are similar, but are actually subtly different. With interfaces, we could use an extends
clause to extend from other types, and we were able to do something similar with intersections and name the result with a type alias. The principle difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you'd pick one over the other between an interface and a type alias of an intersection type.
Let's imagine a Box
type that can contain any value - string
s, number
s, Giraffe
s, whatever.
Right now, the contents
property is typed as any
, which works, but can lead to accidents down the line.
We could instead use unknown
, but that would mean that in cases where we already know the type of contents
, we'd need to do precautionary checks, or use error-prone type assertions.
One type safe approach would be to instead scaffold out different Box
types for every type of contents
.
But that means we'll have to create different functions, or overloads of functions, to operate on these types.
That's a lot of boilerplate. Moreover, we might later need to introduce new types and overloads. This is frustrating, since our box types and overloads are all effectively the same.
Instead, we can make a generic Box
type which declares a type parameter.
You might read this as "A Box
of Type
is something whose contents
have type Type
". Later on, when we refer to Box
, we have to give a type argument in place of Type
.
Think of Box
as a template for a real type, where Type
is a placeholder that will get replaced with some other type. When TypeScript sees Box<string>
, it will replace every instance of Type
in Box<Type>
with string
, and end up working with something like { contents: string }
. In other words, Box<string>
and our earlier StringBox
work identically.
Box
is reusable in that Type
can be substituted with anything. That means that when we need a box for a new type, we don't need to declare a new Box
type at all (though we certainly could if we wanted to).
This also means that we can avoid overloads entirely by instead using [generic functions](https: //www.typescriptlang.org/docs/handbook/2/functions.html#generic-functions).
It is worth noting that type aliases can also be generic. We could have defined our new Box<Type>
interface, which was:
by using a type alias instead:
Since type aliases, unlike interfaces, can describe more than just object types, we can also use them to write other kinds of generic helper types.
We'll circle back to type aliases in just a little bit.
Array
TypeGeneric object types are often some sort of container type that work independently of the type of elements they contain. It's ideal for data structures to work this way so that they're re-usable across different data types.
It turns out we've been working with a type just like that throughout this handbook: the Array
type. Whenever we write out types like number[]
or string[]
, that's really just a shorthand for Array<number>
and Array<string>
.
Much like the Box
type above, Array
itself is a generic type.
Modern JavaScript also provides other data structures which are generic, like Map<K, V>
, Set<T>
, and Promise<T>
. All this really means is that because of how Map
, Set
, and Promise
behave, they can work with any sets of types.
ReadonlyArray
TypeThe ReadonlyArray
is a special type that describes arrays that shouldn't be changed.
Much like the readonly
modifier for properties, it's mainly a tool we can use for intent. When we see a function that returns ReadonlyArray
s, it tells us we're not meant to change the contents at all, and when we see a function that consumes ReadonlyArray
s, it tells us that we can pass any array into that function without worrying that it will change its contents.
Unlike Array
, there isn't a ReadonlyArray
constructor that we can use.
Instead, we can assign regular Array
s to ReadonlyArray
s.
Just as TypeScript provides a shorthand syntax for Array<Type>
with Type[]
, it also provides a shorthand syntax for ReadonlyArray<Type>
with readonly Type[]
.
One last thing to note is that unlike the readonly
property modifier, assignability isn't bidirectional between regular Array
s and ReadonlyArray
s.
A tuple type is another sort of Array
type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.
Here, StringNumberPair
is a tuple type of string
and number
. Like ReadonlyArray
, it has no representation at runtime, but is significant to TypeScript. To the type system, StringNumberPair
describes arrays whose 0
index contains a string
and whose 1
index contains a number
.
If we try to index past the number of elements, we'll get an error.
We can also [destructure tuples](https: //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring) using JavaScript's array destructuring.
Tuple types are useful in heavily convention-based APIs, where each element's meaning is "obvious". This gives us flexibility in whatever we want to name our variables when we destructure them. In the above example, we were able to name elements
0
and1
to whatever we wanted.However, since not every user holds the same view of what's obvious, it may be worth reconsidering whether using objects with descriptive property names may be better for your API.
Other than those length checks, simple tuple types like these are equivalent to types which are versions of Array
s that declare properties for specific indexes, and that declare length
with a numeric literal type.
Another thing you may be interested in is that tuples can have optional properties by writing out a question mark (?
after an element's type). Optional tuple elements can only come at the end, and also affect the type of length
.
Tuples can also have rest elements, which have to be an array/tuple type.
StringNumberBooleans
describes a tuple whose first two elements are string
and number
respectively, but which may have any number of boolean
s following.
StringBooleansNumber
describes a tuple whose first element is string
and then any number of boolean
s and ending with a number
.
BooleansStringNumber
describes a tuple whose starting elements any number of boolean
s and ending with a string
then a number
.
A tuple with a rest element has no set "length" - it only has a set of well-known elements in different positions.
Why might optional and rest elements be useful? Well, it allows TypeScript to correspond tuples with parameter lists. Tuples types can be used in [rest parameters and arguments](https: //www.typescriptlang.org/docs/handbook/2/functions.html#rest-parameters-and-arguments), so that the following:
is basically equivalent to:
This is handy when you want to take a variable number of arguments with a rest parameter, and you need a minimum number of elements, but you don't want to introduce intermediate variables.
readonly
Tuple TypesOne final note about tuple types - tuples types have readonly
variants, and can be specified by sticking a readonly
modifier in front of them - just like with array shorthand syntax.
As you might expect, writing to any property of a readonly
tuple isn't allowed in TypeScript.
Tuples tend to be created and left un-modified in most code, so annotating types as readonly
tuples when possible is a good default. This is also important given that array literals with const
assertions will be inferred with readonly
tuple types.
Here, distanceFromOrigin
never modifies its elements, but expects a mutable tuple. Since point
's type was inferred as readonly [3, 4]
, it won't be compatible with [number, number]
since that type can't guarantee point
's elements won't be mutated.
When we talk about a type in TypeScript, we mean a collection of things that you can do with a variable (or expression). You might be able to read or write a given property, call a function, use the expression as a constructor, or index into the object. Some objects (like Date) in JavaScript can do nearly all of those! In TypeScript, interfaces are the most flexible way of describing types.
You'll see interfaces used to describe existing JavaScript APIs, create shorthand names for commonly-used types, constrain class implementations, describe array types, and more. While they don't generate any code (and thus have no runtime cost!), they are often the key point of contact between any two pieces of TypeScript code, especially when working with existing JavaScript code or built-in JavaScript objects.
The only job of an interface in TypeScript is to describe a type. While class and function deal with implementation, interface helps us keep our programs error-free by providing information about the shape of the data we work with. Because the type information is erased from a TypeScript program during compilation, we can freely add type data using interfaces without worrying about the runtime overhead.
While that sounds like a simple, one-purpose task, interfaces role in describing types becomes manifest in a large variety of ways. Let's look at some of them and how they can be used in TypeScript programs.
To define an interface in TypeScript, use the interface keyword:
This defines a type, Greetable, that has a member function called greet that takes a string argument. You can use this type in all the usual positions; for example in a parameter type annotation. Here we use that type annotation to get type safety on the g parameter:
When this code compiles, you won't see any mention of Greetable in the JavaScript code. Interfaces are only a compile-time construct and have no effect on the generated code.
Interfaces get to play a lot of roles in TypeScript code. We'll go into more detail on these after a quick overview.
Describing an Object
Many JavaScript functions take a "settings object". For example, jQuery's $.ajax takes an object that can have up to several dozen members that control its behavior, but you're only likely to pass a few of those in any given instance. TypeScript interfaces allow optional properties to help you use these sorts of objects correctly.
Describing an Indexable Object
JavaScript freely mixes members (foo.x) with indexers (foo['x']), but most programmers use one or the other as a semantic hint about what kind of access is taking place. TypeScript interfaces can be used to represent what the expected type of an indexing operation is.
Ensuring Class Instance Shape
Often, you'll want to make sure that a class you're writing matches some existing surface area. This is how interfaces are used in more traditional OOP languages like C# and Java, and we'll see that TypeScript interfaces behave very similarly when used in this role.
Ensuring the Static Shape of a Class or constructor Object
Interfaces normally describe the shape of an instance of a class, but we can also use them to describe the static shape of the class (including its constructor function). We'll cover this in a later post.
You can also use interfaces to define the shape of objects that will typically be expressed in an object literal. Here's an example:
Describing Simple Types
Note the use of the ? symbol after some of the names. This marks a member as being optional. This lets callers of createButton supply only the members they care about, while maintaining the constraint that the required parts of the object are present:
You typically won't use optional members when defining interfaces that are going to be implemented by classes.
Here's another example that shows an interesting feature of types in TypeScript:
Note that we didn't annotate pt in any way to indicate that it's of type Point. We don't need to, because type checking in TypeScript is structural: types are considered identical if they have the same surface area. Because pt has at least the same members as Point, it's suitable for use wherever a Point is expected.
Describing External Types
Interfaces are also used to describe code that is present at runtime, but not implemented in the current TypeScript project. For example, if you open the lib.d.ts file that all TypeScript projects implicitly reference, you'll see an interface declaration for Number:
Now if we have an expression of type Number, the compiler knows that it's valid to call toPrecision on that expression.
Extending Existing Types
Moreover, interfaces in TypeScript are open, meaning you can add your own members to an interface by simply writing another interface block. If you have an external script that adds members to Date, for example, you simply need to write interface Date { /*...*/ } and declare the additional members.*
* Note: There are some known issues with the Visual Studio editor that currently prevent this scenario from working as intended. We'll be fixing this limitation in a later release.
A common pattern in JavaScript is to use an object (e.g. {}) as way to map from a set of strings to a set of values. When those values are of the same type, you can use an interface to describe that indexing into an object always produces values of a certain type (in this case, Widget).
Let's extend the Greetable example above:
We can implement this interface in a class using the implements keyword:
Now we can use an instance of Person wherever a Greetable is expected:
var g: Greetable = new Person();
Similarly, we can take advantage of the structural typing of TypeScript to implement Greetable in an object literal:
In typescript global types can be declared in a .d.ts
file and used anywhere without explicitly importing them. Our project's .d.ts
file is named project.d.ts
.
It contains:
Some library types in the form of [triple slash directives](https: //www.typescriptlang.org/docs/handbook/triple-slash-directives.html). These need to be placed at the top of the file.
Some library module declarations (usually these are included because these libs don't have typings but we still need to use them).
Our own global types.
Typescript provides many [Utility Types](https: //www.typescriptlang.org/docs/handbook/utility-types.html) which are useful for manipulating the base types in the global ComponentTypes interface.
A few basic ones to know:
Pick<Type, Keys>
Only use the specified Keys from the Type.
Partial<Type>
Allows the type to be optional (undefined)
Required<Type>
Opposite of Partial, the type must be defined
Using the stategies above you can select types from the global source and compose them to create a representation of the props in a specific component. While the global types live in project.d.ts
, component level types should generally be placed in a types.ts
file within the component directory and imported for use.
Naming
{['class', 'enum', 'interface', 'namespace', 'type', 'variable-and-function'].map(item => (
{item.split('-').join(' ')}
))}
class
🧑🔬 PascalCase
For memebers/methods use 🐪 camelCase
enum
🧑🔬 PascalCase
interface
🧑🔬 PascalCase
For memebers use 🐪 camelCase
namespace
🧑🔬 PascalCase
type
🧑🔬 PascalCase
variable and function
🐪 camelCase
React | Typescript | Tailwind | Forms | Unit Tests
Although ComponentTypes is a _Good starting place, some components may require a type that is more specific and not usefully included in the global declaration._
Bad
Good
Bad
Good
Bad
Good
Bad
Good
Bad
Good
Bad
Good
Bad
✅ Good
Bad
Good