TypeScript Basics
Each and every value in JavaScript has a set of behaviors you can observe from running different operations.
That sounds abstract, but as a quick example, consider some operations we might run on a variable named foo
.
// accessing the property 'toLowerCase' // on 'foo' and then calling it foo.toLowerCase(); // calling 'foo' foo();Try
If we break this down, the first runnable line of code accesses a property called toLowerCase
and then calls it.
The second one tries to call foo
directly.
But assuming we don't know the value of foo
- and that's pretty common - we can't reliably say what results we'll get from trying to run any of this code.
The behavior of each operation depends entirely on what what value we had in the first place.
Is foo
callable?
Does it have a property called toLowerCase
on it?
And if it does, is toLowerCase
callable?
If all of these values are callable, what do they return?
The answers to these questions are usually things we keep in our heads when we write JavaScript, and we have to hope we got all the details right.
Let's say foo
was defined in the following way.
let foo = "Hello World!";
As you can probably guess, if we try to run foo.toLowerCase()
, we'll get the same string, but completely in lower-case letters.
What about that second line of code? If you're familiar with JavaScript, you'll know this fails with an exception:
TypeError: foo is not a function
It'd be great if we could avoid mistakes like this.
When we run our code, the way that our JavaScript runtime chooses what to do is by figuring out the type of the value - what sorts of behaviors and capabilities it has.
That's part of what that TypeError
is alluding to - it's saying that there's nothing to call on the string "Hello World"
.
For some values, such as the primitives string
and number
, we can identify their type at runtime using the typeof
operator.
But for other things like functions, there's no corresponding runtime mechanism to identify their types.
For example, consider this function:
function fn(x) { return x.flip(); }Try
We can observe by reading the code that this function will only work if given an object with a callable flip
property, but JavaScript doesn't surface this information in a way that we can check while the code is running.
The only way in pure JavaScript to tell what fn
does with a particular value is to call it and see what happens.
This kind of behavior makes it hard to predict what code will do before it runs, which means it's harder to know what your code is going to do while you're writing it.
Seen in this way, a type is the concept of describing which values are legal to pass to fn
and which aren't legal.
JavaScript only truly provides dynamic typing - running the code to see what happens.
The alternative is to use a static type system to make predictions about what code is legal before it runs.
Static type-checking
Think back to that TypeError
we got earlier from calling a string
.
Most people don't like to get any sorts of errors when running their code - those are considered bugs!
And when we write new code, we try our best to avoid introducing new bugs.
If we add just a bit of code, save our file, refresh our app, and immediately see the error, we might be able to isolate the problem quickly; but that's not always the case. We might not have tested the feature thoroughly enough, so we might never actually run into a potential error that would be thrown! Or if we were lucky enough to witness the error, we might have ended up doing large refactorings and adding a lot of different code that we're forced to dig through.
Ideally, we could have a tool that helps us find these bugs before our code runs. That's what a static type-checker like TypeScript does. Static types systems describe the shapes and behaviors of what our values will be when we run our programs. A type-checker like TypeScript uses that information and tells us when things might be going off the rails.
let foo = "hello!"; foo()Cannot invoke an expression whose type lacks a call signature. Type 'String' has no compatible call signatures.;TryCannot invoke an expression whose type lacks a call signature. Type 'String' has no compatible call signatures.
Running that last sample with TypeScript will give us an error message before we run the code in the first place.
Non-exception Failures
So far we've been discussing certain things like runtime errors - cases where the JavaScript runtime throws its hands up and tells us that it thinks something is nonsensical. Those cases come up because the ECMAScript specification has explicit instructions on how the language should behave when it runs into something unexpected.
For example, the specification says that trying to call something that isn't callable should throw an error.
Maybe that sounds like "obvious behavior", but you could imagine that accessing a property that doesn't exist on an object should throw an error too.
Instead, JavaScript gives us different behavior and returns the value undefined
:
let foo = { name: "Daniel", age: 26, }; foo.location; // returns undefinedTry
Ultimately, a static type system has to make the call over what code should be flagged as an error in its system, even if it's "valid" JavaScript that won't immediately throw an error.
In TypeScript, the following code produces an error about location
not being defined:
let foo = { name: "Daniel", age: 26, }; foo.locationProperty 'location' does not exist on type '{ name: string; age: number; }'.; // returns undefinedTryProperty 'location' does not exist on type '{ name: string; age: number; }'.
While sometimes that implies a trade-off in what you can express, the intent is to catch legitimate bugs in our programs. And TypeScript catches a lot of legitimate bugs. For example: typos,
let someString = "Hello World!"; // How quickly can you spot the typos? someString.toLocaleLowercaseProperty 'toLocaleLowercase' does not exist on type 'string'. Did you mean 'toLocaleLowerCase'?(); someString.toLocalLowerCaseProperty 'toLocalLowerCase' does not exist on type 'string'. Did you mean 'toLocaleLowerCase'?(); // We probably meant to write this... someString.toLocaleLowerCase();Property 'toLocaleLowercase' does not exist on type 'string'. Did you mean 'toLocaleLowerCase'?TryProperty 'toLocalLowerCase' does not exist on type 'string'. Did you mean 'toLocaleLowerCase'?
uncalled functions,
function flipCoin() { return Math.random < 0.5Operator '<' cannot be applied to types '() => number' and 'number'.; }TryOperator '<' cannot be applied to types '() => number' and 'number'.
or basic logic errors.
const value = Math.random() < 0.5 ? "a" : "b"; if (value !== "a") { // ... } else if (value === "b"This condition will always return 'false' since the types '"a"' and '"b"' have no overlap.) { // Oops, unreachable }TryThis condition will always return 'false' since the types '"a"' and '"b"' have no overlap.
Types for Tooling
TypeScript can catch bugs when we make mistakes in our code. That's great, but TypeScript can also prevent us from making those mistakes in the first place.
The type-checker has information to check things like whether we're accessing the right properties on variables and other properties. Once it has that information, it can also start suggesting which properties you might want to use.
That means TypeScript can be leveraged for editing code too, and the core type-checker can provide error messages and code completion as you type in the editor. That's part of what people often refer to when they talk about tooling in TypeScript.
TypeScript takes tooling seriously, and that goes beyond completions and errors as you type. An editor that supports TypeScript can deliver "quick fixes" to automatically fix errors, refactorings to easily re-organize code, and useful navigation features for jumping to definitions of a variable, or finding all references to a given variable. All of this is built on top of the type-checker and fully cross-platform, so it's likely that your favorite editor has TypeScript support available.
tsc
, the TypeScript compiler
We've been talking about type-checking, but we haven't yet used our type-checker.
Let's get acquainted with our new friend tsc
, the TypeScript compiler.
First we'll need to grab it via npm.
npm install -g typescript
This installs the TypeScript Compiler
tsc
globally. You can usenpx
or similar tools if you'd prefer to runtsc
from a localnode_modules
package instead.
Now let's move to an empty folder and try writing our first TypeScript program: hello.ts
:
// Greets the world. console.log("Hello world!");
Notice there are no frills here; this "hello world" program looks identical to what you'd write for a "hello world" program in JavaScript.
And now let's type-check it by running the command tsc
which was installed for us by the typescript
package.
tsc hello.ts
Tada!
Wait, "tada" what exactly?
We ran tsc
and nothing happened!
Well, there were no type errors, so we didn't get any output in our console since there was nothing to report.
But check again - we got some file output instead.
If we look in our current directory, we'll see a hello.js
file next to hello.ts
.
That's the output from our hello.ts
file after tsc
compiles or transforms it into a JavaScript file.
And if we check the contents, we'll see what TypeScript spits out after it processes a .ts
file:
// Greets the world. console.log("Hello world!");
In this case, there was very little for TypeScript to transform, so it looks identical to what we wrote. The compiler tries to emit clean readable code that looks like something a person would write. While that's not always so easy, TypeScript indents consistently, is mindful of when our code spans across different lines of code, and tries to keep comments around.
What about if we did introduce a type-checking error?
Let's rewrite hello.ts
:
// This is an industrial-grade general-purpose greeter function: function greet(person, date) { console.log(`Hello ${person}, today is ${date}!`); } greet("Brendan")Expected 2 arguments, but got 1.;TryExpected 2 arguments, but got 1.
If we run tsc hello.ts
again, notice that we get an error on the command line!
Expected 2 arguments, but got 1.
TypeScript is telling us we forgot to pass an argument to the greet
function, and rightfully so.
So far we've only written standard JavaScript, and yet type-checking was still able to find problems with our code.
Thanks TypeScript!
Emitting with Errors
One thing you might not have noticed from the last example was that our hello.js
file changed again.
If we open that file up then we'll see that the contents still basically look the same as our input file.
That might be a bit surprising given the fact that tsc
reported an error about our code, but this based on one of TypeScript's core values: much of the time, you will know better than TypeScript.
To reiterate from earlier, type-checking code limits the sorts of programs you can run, and so there's a tradeoff on what sorts of things a type-checker finds acceptable. Most of the time that's okay, but there are scenarios where those checks get in the way. For example, imagine yourself migrating JavaScript code over to TypeScript and introducing type-checking errors. Eventually you'll get around to cleaning things up for the type-checker, but that original JavaScript code was already working! Why should converting it over to TypeScript stop you from running it?
So TypeScript doesn't get in your way.
Of course, over time, you may want to be a bit more defensive against mistakes, and make TypeScript act a bit more strictly.
In that case, you can use the --noEmitOnError
compiler option.
Try changing your hello.ts
file and running tsc
with that flag:
tsc --noEmitOnError hello.ts
You'll notice that hello.js
never gets updated.
Explicit Types
Up until now, we haven't told TypeScript what person
or date
are.
Let's change up our code a little bit so that we tell TypeScript that person
is a string
, and that date
should be a Date
object.
We'll also use the toDateString()
method on date
.
function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); }Try
What we did was add type annotations on person
and date
to describe what types of values greet
can be called with.
You can read that signature as "greet
takes a person
of type string
, and a date
of type Date
".
With this, TypeScript can tell us about other cases where we might have been called incorrectly. For example…
function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet("Maddison", Date()Argument of type 'string' is not assignable to parameter of type 'Date'.);TryArgument of type 'string' is not assignable to parameter of type 'Date'.
Huh? TypeScript reported an error on our second argument, but why?
Perhaps surprisingly, calling Date()
in JavaScript returns a string
.
On the other hand, constructing a Date
with new Date()
actually gives us what we were expecting.
Anyway, we can quickly fix up the error:
function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet("Maddison", new Date());Try
Keep in mind, we don't always have to write explicit type annotations. In many cases, TypeScript can even just infer (or "figure out") the types for us even if we omit them.
let foo = "hello there!" ▲let foo: string
Even though we didn't tell TypeScript that foo
had the type string
it was able to figure that out.
That's a feature, and it's best not to add annotations when the type system would end up inferring the same type anyway.
Erased Types
Let's take a look at what happens when we compile with tsc
:
function greet(person, date) { console.log("Hello " + person + ", today is " + date.toDateString() + "!"); } greet("Maddison", new Date());Try
Notice two things here:
- Our
person
anddate
parameters no longer have type annotations. - Our "template string" - that string that used backticks (the
`
character - was converted to plain strings with concatenations (+
).
More on that second point later, but let's now focus on that first point. Type annotations aren't part of JavaScript (or ECMAScript to be pedantic), so there really aren't any browsers or other runtimes that can just run TypeScript unmodified. That's why TypeScript needs a compiler in the first place - it needs some way to strip out or transform any TypeScript-specific code so that you can run it. Most TypeScript-specific code gets erased away, and likewise, here our type annotations were completely erased.
Remember: Type annotations never change the runtime behavior of your program.
Downleveling
One other difference from the above was that our template string was rewritten from
`Hello ${person}, today is ${date.toDateString()}!`
to
"Hello " + person + ", today is " + date.toDateString() + "!"
Why did this happen?
Template strings are a feature from a version of ECMAScript called ECMAScript 2015 (a.k.a. ECMAScript 6, ES2015, ES6, etc. - don't ask). TypeScript has the ability to rewrite code from newer versions of ECMAScript to older ones such as ECMAScript 3 or ECMAScript 5 (a.k.a. ES3 and ES5). This process from moving from a newer or "higher" version of ECMAScript to an older or "lower" one is sometimes called downleveling.
By default TypeScript targets ES3, an extremely old version of ECMAScript.
We could have chosen something a little bit more recent by using the --target
flag.
Running with --target es2015
changes TypeScript to target ECMAScript 2015, meaning code should be able to run wherever ECMAScript 2015 is supported.
So running tsc --target es2015 input.ts
gives us the following output:
function greet(person, date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet("Maddison", new Date());Try
While the default target is ES3, the great majority of running browsers support ES5. Today, most developers can safely specify ES5 or even ES2016 as a target unless compatibility with certain ancient browers is important.
Strictness
Users come to TypeScript looking for different things in a type-checker.
Some people are looking for a more loose opt-in experience which can help validate only some parts of our program and give us decent tooling.
This is the default experience with TypeScript, where types are optional, inference takes the most lenient types, and there's no checking for potentially null
/undefined
values.
Much like how tsc
emits in the face of errors, these defaults are put in place to stay out of your way.
If you're migrating existing JavaScript, that might be desirable.
In contrast, a lot of users prefer to have TypeScript validate as much as it can off the bat, and that's why the language provides strictness settings as well. These strictness settings turn static type-checking from a switch (either your code is checked or not) into something closer to a dial. The farther you turn this dial up, the more TypeScript will check for you. This can require a little extra work, but generally speaking it pays for itself in the long run, and enables more thorough checks and more accurate tooling. If possible, a new codebase should always turn these strictness checks on.
TypeScript has several type-checking strictness flags that can be turned on or off, and all of our examples will be written with all of them enabled unless otherwise stated.
The --strict
flag toggles them all on simultaneously, but we can opt out of them individually.
The two biggest ones you should know about are noImplicitAny
and strictNullChecks
.
noImplicitAny
Recall that in some places, TypeScript doesn't try to infer any types for us and instead falls back to the most lenient type: any
.
This isn't the worst thing that can happen - after all, falling back to any
is just the JavaScript experience anyway.
However, using any
often defeats the purpose of using TypeScript in the first place.
The more typed your program is, the more validation and tooling you'll get, meaning you'll run into fewer bugs as you code.
Turning on the noImplicitAny
flag will issue an error on any variables whose type is implicitly inferred as any
.
strictNullChecks
By default, values like null
and undefined
are assignable to any other type.
This can make writing some code easier, but forgetting to handle null
and undefined
is the cause of countless bugs in the world - not even just JavaScript!
The strictNullChecks
flag makes handling null
and undefined
more explicit, and spares us from worrying about whether we forgot to handle null
and undefined
.
Everyday Types
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.
Primitives string
, number
, and boolean
JavaScript has three main primitive kinds of values: 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 shouldn't appear in your code. Always usestring
,number
, orboolean
.
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:
let obj: any = { x: 0 }; // None of these lines of code are errors obj.foo(); obj(); obj.bar = 100; obj = "hello"; const n: number = obj;Try
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 a type isn't specified and can't be inferred from context, TypeScript will typically default to any
.
Because any
values don't benefit from type-checking, it's usually desirable to avoid these situations.
The compiler flag noImplicitAny
will cause any implicit any
to be flagged 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:
let myNameType annotation: 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:
// No type annotation needed -- 'myName' inferred as type 'string' 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.
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 kinds of parameters the function accepts. Parameter type annotations go after the parameter name:
// Parameter type annotation function greet(name: string) { console.log("Hello, " + name.toUpperCase() + "!!"); }Try
When a parameter has a type annotation, calls to that function will be validated:
// Would be a runtime error if executed! greet(42Argument of type '42' is not assignable to parameter of type 'string'.);Argument of type '42' is not assignable to parameter of type 'string'.
Return Type Annotations
You can also add return type annotations. Return type annotations appear after the parameter list:
function getFavoriteNumber(): number { return 26; }Try
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.
Function Expressions
Function expressions are a little bit different from function declarations. When a function expression 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:
// No type annotations here, but TypeScript can spot the bug const names = ["Alice", "Bob", "Eve"]; names.forEach(function (s) { console.log(s.toUppercaseProperty 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?()); });TryProperty '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.
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 type function printCoord(pt: { x: number, y: number }) { console.log("The coordinate's x value is " + pt.x); console.log("The coordinate's y value is " + pt.y); } printCoord({ x: 3, y: 7 });Try
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 OK printName({ first: "Bob" }); printName({ first: "Alice", last: "Alisson" });Try
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.lastObject is possibly 'undefined'..toUpperCase()); if (obj.last !== undefined) { // OK console.log(obj.last.toUpperCase()); } }TryObject is possibly 'undefined'.
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); } // OK printId(101); // OK printId("202"); // Error printId([1, 2]Argument of type 'number[]' is not assignable to parameter of type 'string | number'. Type 'number[]' is not assignable to type 'string'.);TryArgument of type 'number[]' is not assignable to parameter of type 'string | number'.Type 'number[]' is not assignable to type 'string'.
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.toUpperCaseProperty 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.()); }TryProperty '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); } }Try
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); } }Try
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[] | string function getFirstThree(x: number[] | string) { return x.slice(0, 3); }Try
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 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 example function printCoord(pt: Point) { console.log("The coordinate's x value is " + pt.x); console.log("The coordinate's y value is " + pt.y); } printCoord({ x: 100, y: 100 });Try
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 Age = number; type Weight = number; const myAge: Age = 73; // *not* an error const myWeight: Weight = myAge;Try
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 });Try
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. Here are the most relevant differences between the two that you should be aware of. You'll learn more about these concepts in later chapters, so don't worry if you don't understand all of these right away.
- Interfaces may be
extend
ed, but not type aliases. We'll discuss this later, but it means that interfaces can provide more guarantees when creating new types out of other types. - Type aliases may not participate in declaration merging, but interfaces can.
- Interfaces may only be used to declare object types.
- Interface names will always appear in their original form in error messages, but only when they are used by name.
- Type alias names may appear in error messages, sometimes in place of the equivalent anonymous type (which may or may not be desirable).
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.
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 they 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:
const x = "hello" as numberConversion 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.;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.
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.
By themselves, literal types aren't very valuable:
let x: "hello" = "hello"; // OK x = "hello"; // OK x = "hello"; // ... xType '"howdy"' is not assignable to type '"hello"'. = "howdy";TryType '"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 thing - 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"'.);TryArgument 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; }Try
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"'.);TryArgument 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; }Try
TypeScript doesn't assume the assignment of 1
to a field that previously had 0
to be 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.methodArgument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.);Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
Because it'd be legal to assign a string like "GUESS"
TO req.method
, TypeScript considers this code to have an error.
You can change this inference by adding a type assertion in either location:
const req = { url: "https://example.com", method: "GET" as "GET" }; /* or */ handleRequest(req.url, req.method as "GET");Try
The first change means "I intend for req.method
to always have the literal type "GET"
", preventing the possible assignment of "GUESS"
to that field.
The second change means "I know for other reasons that req.method
has the value "GET"
".
null
and undefined
JavaScript has two primitive values, null
and undefined
, both of which are used to signal absent or uninitialized values.
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 | null) { if (x === null) { // do nothing } else { console.log("Hello, " + x.toUpperCase()); } }Try
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 | null) { // No error console.log(x!.toFixed()); }Try
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
.
Type Declarations
Throughout the sections you've read so far, we've been demonstrating basic TypeScript concepts using the built-in functions present in all JavaScript runtimes. However, almost all JavaScript today includes many libraries to accomplish common tasks. Having types for the parts of your application that aren't your code will greatly improve your TypeScript experience. Where do these types come from?
What Do Type Declarations Look Like?
Let's say you write some code like this:
const k = Math.max(5, 6); const j = Math.mixProperty 'mix' does not exist on type 'Math'.(7, 8);Property 'mix' does not exist on type 'Math'.
How did TypeScript know that max
was present but not mix
, even though Math
's implementation wasn't part of your code?
The answer is that there are declaration files describing these built-in objects. A declaration file provides a way to declare the existence of some types or values without actually providing implementations for those values.
.d.ts
files
TypeScript has two main kinds of files.
.ts
files are implementation files that contain types and executable code.
These are the files that produce .js
outputs, and are where you'd normally write your code.
.d.ts
files are declaration files that contain only type information.
These files don't produce .js
outputs; they are only used for typechecking.
We'll learn more about how to write our own declaration files later.
Built-in Type Definitions
TypeScript includes declaration files for all of the standardized built-in APIs available in JavaScript runtimes.
This includes things like methods and properties of built-in types like string
or function
, top-level names like Math
and Object
, and their associated types.
By default, TypeScript also includes types for things available when running inside the browser, such as window
and document
; these are collectively referred to as the DOM APIs.
TypeScript names these declaration files with the pattern lib.[something].d.ts
.
If you navigate into a file with that name, you can know that you're dealing with some built-in part of the platform, not user code.
target
setting
The methods, properties, and functions available to you actually vary based on the version of JavaScript your code is running on.
For example, the startsWith
method of strings is available only starting with the version of JavaScript referred as ECMAScript 6.
Being aware of what version of JavaScript your code ultimately runs on is important because you don't want to use APIs that are from a newer version than the platform you deploy to.
This is one function of the target
compiler setting.
TypeScript helps with this problem by varying which lib
files are included by default based on your target
setting.
For example, if target
is ES5
, you will see an error if trying to use the startsWith
method, because that method is only available in ES6
or later.
lib
setting
The lib
setting allows more fine-grained control of which built-in declaration files are considered available in your program.
See the documentation page on lib for more information.
External Definitions
For non-built-in APIs, there are a variety of ways you can get declaration files. How you do this depends on exactly which library you're getting types for.
Bundled Types
If a library you're using is published as an npm package, it may include type declaration files as part of its distribution already. You can read the project's documentation to find out, or simply try importing the package and see if TypeScript is able to automatically resolve the types for you.
If you're a package author considering bundling type definitions with your package, you can read our guide on bundling type definitions.
DefinitelyTyped / @types
The DefinitelyTyped repository is a centralized repo storing declaration files for thousands of libraries. The vast majority of commonly-used libraries have declaration files available on DefinitelyTyped.
Definitions on DefinitelyTyped are also automatically published to npm under the @types
scope.
The name of the types package is always the same as the name of the underlying package itself.
For example, if you installed the react
npm package, you can install its corresponding types by running
npm install --save-dev @types/react
TypeScript automatically finds type definitions under node_modules/@types
, so there's no other step needed to get these types available in your program.
Your Own Definitions
In the uncommon event that a library didn't bundle its own types and didn't have a definition on DefinitelyTyped, you can write a declaration file yourself. See the appendix Writing Declaration Files for a guide.
If you want to silence warnings about a particular module without writing a declaration file, you can also quick declare the module as type any
by putting an empty declaration for it in a .d.ts
file in your project.
For example, if you wanted to use a module named some-untyped-module
without having definitions for it, you would write:
declare module "some-untyped-module";
Narrowing
Imagine we have a function called padLeft
.
function padLeft(padding: number | string, input: string): string { throw new Error("Not implemented yet!"); }Try
If padding
is a number
, it will treat that as the number of spaces we want to prepend to input
.
If padding
is a string
, it should just prepend padding
to input
.
Let's try to implement the logic for when padLeft
is passed a number
for padding
.
function padLeft(padding: number | string, input: string) { return new Array(padding + 1Operator '+' cannot be applied to types 'string | number' and '1'.).join(" ") + input; }TryOperator '+' cannot be applied to types 'string | number' and '1'.
Uh-oh, we're getting an error on padding + 1
.
TypeScript is warning us that adding a number
to a number | string
might not give us what we want, and it's right.
In other words, we haven't explicitly checked if padding
is a number
first, nor are we handling the case where it's a string
, so let's do exactly that.
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return new Array(padding + 1).join(" ") + input; } return padding + input; }Try
If this mostly looks like uninteresting JavaScript code, that's sort of the point. Apart from the annotations we put in place, this TypeScript code looks like JavaScript. The idea is that TypeScript's type system aims to make it as easy as possible to write typical JavaScript code without bending over backwards to get type safety.
While it might not look like much, there's actually a lot going under the covers here.
Much like how TypeScript analyzes runtime values using static types, it overlays type analysis on JavaScript's runtime control flow constructs like if/else
, conditional ternaries, loops, truthiness checks, etc., which can all affect those types.
Within our if
check, TypeScript sees typeof padding === "number"
and understands that as a special form of code called a type guard.
TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position.
It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing.
In many editors we can observe these types as they change, and we'll even do so in our examples.
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return new Array(padding + 1).join(" ") + input; ▲(parameter) padding: number } return padding + input; ▲(parameter) padding: string }Try
There are a couple of different constructs TypeScript understands for narrowing.
typeof
type guards
As we've seen, JavaScript supports a typeof
operator which can give very basic information about the type of values we have at runtime.
TypeScript expects this to return a certain set of strings:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
Like we saw with padLeft
, this operator comes up pretty often in a number of JavaScript libraries, and TypeScript can understand it to narrow types in different branches.
In TypeScript, checking against the value returned by typeof
is a type guard.
Because TypeScript encodes how typeof
operates on different values, it knows about some of its quirks in JavaScript.
For example, notice that in the list above, typeof
doesn't return the string null
.
Check out the following example:
function printAll(strs: string | string[] | null) { if (typeof strs === "object") { for (const s of strsObject is possibly 'null'.) { console.log(s) } } else if (typeof strs === "string") { console.log(strs) } else { // do nothing } }TryObject is possibly 'null'.
In the printAll
function, we try to check if strs
is an object to see if it's an array type (now might be a good time to reinforce that arrays are object types in JavaScript).
But it turns out that in JavaScript, typeof null
is actually "object"
!
This is one of those unfortunate accidents of history.
Users with enough experience might not be surprised, but not everyone has run into this in JavaScript; luckily, TypeScript lets us know that strs
was only narrowed down to string[] | null
instead of just string[]
.
This might be a good segue into what we'll call "truthiness" checking.
Truthiness narrowing
Truthiness might not be a word you'll find in the dictionary, but it's very much something you'll hear about in JavaScript.
In JavaScript, we can use any expression in conditionals, &&
s, ||
s, if
statements, and Boolean negations (!
), and more.
As an example, if
statements don't expect their condition to always have the type boolean
.
function getUsersOnlineMessage(numUsersOnline: number) { if (numUsersOnline) { return `There are ${numUsersOnline} online now!`; } return "Nobody's here. :("; }Try
In JavaScript, constructs likeif
first "coerce" their conditions to boolean
s to make sense of them, and then choose their branches depending on whether the result is true
or false
.
Values like
0
NaN
""
(the empty string)0n
(thebigint
version of zero)null
undefined
all coerce to false
, and other values get coerced true
.
You can always coerce values to boolean
s by running them through the Boolean
function, or by using the shorter double-Boolean negation.
// both of these result in 'true' Boolean("hello"); !!"world";Try
It's fairly popular to leverage this behavior, especially for guarding against values like null
or undefined
.
As an example, let's try using it for our printAll
function.
function printAll(strs: string | string[] | null) { if (strs && typeof strs === "object") { for (const s of strs) { console.log(s) } } else if (typeof strs === "string") { console.log(strs) } }Try
You'll notice that we've gotten rid of the error above by checking if strs
is truthy.
This at least prevents us from dreaded errors when we run our code like:
TypeError: null is not iterable
Keep in mind though that truthiness checking on primitives can often be error prone.
As an example, consider a different attempt at writing printAll
function printAll(strs: string | string[] | null) { // !!!!!!!!!!!!!!!! // DON'T DO THIS! // KEEP READING // !!!!!!!!!!!!!!!! if (strs) { if (typeof strs === "object") { for (const s of strs) { console.log(s) } } else if (typeof strs === "string") { console.log(strs) } } }Try
We wrapped the entire body of the function in a truthy check, but this has a subtle downside: we may no longer be handling the empty string case correctly.
TypeScript doesn't hurt us here at all, but this is behavior worth noting if you're less familiar with JavaScript. TypeScript can often help you catch bugs early on, but if you choose to do nothing with a value, there's only so much that it can do without being overly prescriptive. If you want, you can make sure you handle situations like these with a linter.
One last word on narrowing by truthiness is that Boolean negations with !
filter out from negated branches.
function multiplyAll(values: number[] | undefined, factor: number): number[] | undefined { if (!values) { return values; } else { return values.map(x => x * factor); } }Try
Equality narrowing
TypeScript also uses switch
statements and equality checks like ===
, !==
, ==
, and !=
to narrow types.
For example:
function foo(x: string | number, y: string | boolean) { if (x === y) { // We can now call any 'string' method on 'x' or 'y'. x.toUpperCase(); ▲(parameter) x: string y.toLowerCase(); ▲(parameter) y: string } else { console.log(x); ▲(parameter) x: string | number console.log(y); ▲(parameter) y: string | boolean } }Try
When we checked that x
and y
are both equal in the above example, TypeScript knew their types also had to be equal.
Since string
is the only common type that both x
and y
could take on, TypeScript knows that x
and y
must be a string
in the first branch.
Checking against specific literal values (as opposed to variables) works also.
In our section about truthiness narrowing, we wrote a printAll
function which was error-prone because it accidentally didn't handle empty strings properly.
Instead we could have done a specific check to block out null
s, and TypeScript still correctly removes null
from the type of strs
.
function printAll(strs: string | string[] | null) { if (strs !== null) { if (typeof strs === "object") { for (const s of strs) { ▲(parameter) strs: string[] console.log(s); } } else if (typeof strs === "string") { console.log(strs); ▲(parameter) strs: string } } }Try
JavaScript's looser equality checks with ==
and !=
also get narrowed correctly.
If you're unfamiliar, checking whether something == null
actually not only checks whether it is specifically the value null
- it also checks whether it's potentially undefined
.
The same applies to == undefined
: it checks whether a value is either null
or undefined
.
interface Container { value: number | null | undefined } function multiplyValue(container: Container, factor: number) { // Remove both 'null' and 'undefined' from the type. if (container.value != null) { console.log(container.value); ▲(property) Container.value: number // Now we can safely multiply 'container.value'. container.value *= factor; } }Try
instanceof
narrowing
JavaScript has an operator for checking whether or not a value is an "instance" of another value.
More specifically, in JavaScript x instanceof Foo
checks whether the prototype chain of x
contains Foo.prototype
.
While we won't dive deep here, and you'll see more of this when we get into classes, they can still be useful for most values that can be constructed with new
.
As you might have guessed, instanceof
is also a type guard, and TypeScript narrows in branches guarded by instanceof
s.
function logValue(x: Date | string) { if (x instanceof Date) { console.log(x.toUTCString()); ▲(parameter) x: Date } else { console.log(x.toUpperCase()); ▲(parameter) x: string } }Try
Assignments
As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.
let x = Math.random() < 0.5 ? 10 : "hello world!"; ▲let x: string | number x = 1; console.log(x); ▲let x: number x = "goodbye!"; console.log(x); ▲let x: stringTry
Notice that each of these assignments is valid.
Even though the observed type of x
changed to number
after our first assignment, we were still able to assign a string
to x
.
This is because the declared type of x
- the type that x
started with - is string | number
, and assignability is always checked against the declared type.
If we'd assigned a boolean
to x
, we'd have seen an error since that wasn't part of the declared type.
let x = Math.random() < 0.5 ? 10 : "hello world!"; ▲let x: string | number x = 1; console.log(x); ▲let x: number xType 'true' is not assignable to type 'string | number'. = true; console.log(x); ▲let x: string | numberTryType 'true' is not assignable to type 'string | number'.
Control flow analysis
Up until this point, we've gone through some basic examples of how TypeScript narrows within specific branches.
But there's a bit more going on than just walking up from every variable and looking for type guards in if
s, while
s, conditionals, etc.
For example
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return new Array(padding + 1).join(" ") + input; } return padding + input; }Try
padLeft
returns from within its first if
block.
TypeScript was able to analyze this code and see that the rest of the body (return padding + input;
) is unreachable in the case where padding
is a number
.
As a result, it was able to remove number
from the type of padding
(narrowing from string | number
to string
) for the rest of the function.
This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.
function foo() { let x: string | number | boolean; x = Math.random() < 0.5; console.log(x); ▲let x: boolean if (Math.random() < 0.5) { x = "hello"; console.log(x); ▲let x: string } else { x = 100; console.log(x); ▲let x: number } return x; ▲let x: string | number }Try
Discriminated unions
Most of the examples we've looked at so far have focused around narrowing single variables with simple types like string
, boolean
, and number
.
While this is common, most of the time in JavaScript we'll be dealing with slightly more complex structures.
For some motivation, let's imagine we're trying to encode shapes like circles and squares.
Circles keep track of their radii and squares keep track of their side lengths.
We'll use a field called kind
to tell which shape we're dealing with.
Here's a first attempt at defining Shape
.
interface Shape { kind: "circle" | "square"; radius?: number; sideLength?: number; }Try
Notice we're using a union of string literal types: "circle"
and "square"
to tell us whether we should treat the shape as a circle or square respectively.
By using "circle" | "square"
instead of string
, we can avoid misspelling issues.
function handleShape(shape: Shape) { // oops! if (shape.kind === "rect"This condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.) { // ... } }TryThis condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.
We can write a getArea
function that applies the right logic based on if it's dealing with a circle or square.
We'll first try dealing with circles.
function getArea(shape: Shape) { return Math.PI * shape.radiusObject is possibly 'undefined'. ** 2; }TryObject is possibly 'undefined'.
Under strictNullChecks
that gives us an error - which is appropriate since radius
might not be defined.
But what if we perform the appropriate checks on the kind
property?
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radiusObject is possibly 'undefined'. ** 2; } }TryObject is possibly 'undefined'.
Hmm, TypeScript still doesn't know what to do here.
We've hit a point where we know more about our values than the type checker does.
We could try to use a non-null assertion (a !
after shape.radius
) to say that radius
is definitely present.
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius! ** 2; } }Try
But this doesn't feel ideal.
We had to shout a bit at the type-checker with those non-null assertions (!
) to convince it that shape.radius
was defined, but those assertions are error-prone if we start to move code around.
Additionally, outside of strictNullChecks
we're able to accidentally access any of those fields anyway (since optional properties are just assumed to always be present when reading them).
We can definitely do better.
The problem with this encoding of Shape
is that the type-checker doesn't have any way to know whether or not radius
or sideLength
are present based on the kind
property.
We need to communicate what we know to the type checker.
With that in mind, let's take another swing at defining Shape
.
interface Circle { kind: "circle"; radius: number; } interface Square { kind: "square"; sideLength: number; } type Shape = Circle | Square;Try
Here, we've properly separated Shape
out into two types with different values for the kind
property, but radius
and sideLength
are declared as required properties in their respective types.
Let's see what happens here when we try to access the radius
of a Shape
.
function getArea(shape: Shape) { return Math.PI * shape.radiusProperty 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'. ** 2; }TryProperty 'radius' does not exist on type 'Shape'.Property 'radius' does not exist on type 'Square'.
Like with our first definition of Shape
, this is still an error.
When radius
was optional, we got an error (only in strictNullChecks
) because TypeScript couldn't tell whether the property was present.
Now that Shape
is a union, TypeScript is telling us that shape
might be a Square
, and Square
s don't have radius
defined on them!
Both interpretations are correct, but only does our new encoding of Shape
still cause an error outside of strictNullChecks
.
But what if we tried checking the kind
property again?
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius ** 2; ▲(parameter) shape: Circle } }Try
That got rid of the error! When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
In this case, kind
was that common property (which is what's considered a discriminant property of Shape
).
Checking whether the kind
property was "circle"
got rid of every type in Shape
that didn't have a kind
property with the type "circle"
.
That narrowed shape
down to the type Circle
.
The same checking works with switch
statements as well.
Now we can try to write our complete getArea
without any pesky !
non-null assertions.
function getArea(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; ▲(parameter) shape: Circle case "square": return shape.sideLength ** 2; ▲(parameter) shape: Square } }Try
The important thing here was the encoding of Shape
.
Communicating the right information to TypeScript - that Circle
and Square
were really two separate types with specific kind
fields - was crucial.
Doing that let us write type-safe TypeScript code that looks no different than the JavaScript we would've written otherwise.
From there, the type system was able to do the "right" thing and figure out the types in each branch of our switch
statement.
As an aside, try playing around with the above example and remove some of the return keywords. You'll see that type-checking can help avoid bugs when accidentally falling through different clauses in a
switch
statement.
Discriminated unions are useful for more than just talking about circles and squares. They're good for representing any sort of messaging scheme in JavaScript, like when sending messages over the network (client/server communication), or encoding mutations in a state management framework.
The never
type
function getArea(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.sideLength ** 2; } }Try
Exhaustiveness checking
More on Functions
Functions are the basic building block of any application, whether they're local functions, imported from another module, or methods on a class. They're also values, and just like other values, TypeScript has many ways to describe how functions can be called. Let's learn about how to write types that describe functions.
- Function Type Expressions
- Call Signatures
- Construct Signatures
- Generic Functions
- Inference
- Constraints
- Working with Constrained Values
- Specifying Type Arguments
- Guidelines for Writing Good Generic Functions
- Optional Parameters
- Function Overloads
- Other Types to Know About
- Rest Parameters and Arguments
- Parameter Destructuring
- Assignability of Functions
Function Type Expressions
The simplest way to describe a function is with a a function type expression. These types are syntactically similar to arrow functions:
function greeter(fn: (a: string) => void) { fn("Hello, World"); } function printToConsole(s: string) { console.log(s); } greeter(printToConsole);Try
The syntax (a: string) => void
means "a function with one parameter, named a
, of type string, that doesn't have a return value".
Just like with function declarations, if a parameter type isn't specified, it's implicitly any
.
Note that the parameter name is required. The function type
(string) => void
means "a function with a parameter namedstring
of typea
"!
Of course, we can use a type alias to name a function type:
type GreetFunction = (a: string) => void; function greeter(fn: GreetFunction) { // ... }Try
Call Signatures
In JavaScript, functions can have properties in addition to being callable. However, the function type expression syntax doesn't allow for declaring properties. If we want to describe something callable with properties, we can write a call signature in an object type:
type DescribableFunction = { description: string; (someArg: number): boolean; }; function doSomething(fn: DescribableFunction) { console.log(fn.description + " returned " + fn(6)); }Try
Note that the syntax is slightly different compared to a function type expression - use :
between the parameter list and the return type rather than =>
.
Construct Signatures
JavaScript functions can also be invoked with the new
operator.
TypeScript refers to these as constructors because they usually create a new object.
You can write a construct signature by adding the new
keyword in front of a call signature:
type SomeConstructor = { new(s: string): SomeObject; } function fn(ctor: SomeConstructor) { return new ctor("hello"); }Try
Some objects, like JavaScript's Date
object, can be called with or without new
.
You can combine call and construct signatures in the same type arbitrarily:
interface CallOrConstruct { new(s: string): Date; (n?: number): number; }Try
Generic Functions
It's common to write a function where the types of the input relate to the type of the output, or where the types of two inputs are related in some way. Let's consider for a moment a function that returns the first element of an array:
function firstElement(arr: any[]) { return arr[0]; }Try
This function does its job, but unfortunately has the return type any
.
It'd be better if the function returned the type of the array element.
In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature:
function firstElement<T>(arr: T[]): T { return arr[0]; }Try
By adding a type parameter T
to this function and using it in two places, we've created a link between the input of the function (the array) and the output (the return value).
Now when we call it, a more specific type comes out:
// s is of type 'string' const s = firstElement(["a", "b", "c"]); // n is of type 'number' const n = firstElement([1, 2, 3]);Try
Inference
Note that we didn't have to specify T
in this sample.
The type was inferred - chosen automatically - by TypeScript.
We can use multiple type parameters as well.
For example, a standalone version of map
would look like this:
function map<E, O>(arr: E[], func: (arg: E) => O): O[] { return arr.map(func); } // Parameter 'n' is of type 'number' // 'parsed' is of type 'string[]' const parsed = map(["1", "2", "3"], n => parseInt(n));Try
Note that in this example, TypeScript could infer both the type of the E
type parameter (from the given string
array), as well as the type O
based on the return value of the function expression.
Constraints
We've written some generic functions that can work on any kind of value. Sometimes we want to relate two values, but can only operate on a certain subset of values. In this case, we can use a constraint to limit the kinds of types that a type parameter can accept.
Let's write a function that returns the longer of two values.
To do this, we need a length
property that's a number.
We constrain the type parameter to that type by writing an extends
clause:
function longest<T extends { length: number }>(a: T, b: T) { if (a.length >= b.length) { return a; } else { return b; } } // longerArray is of type 'number[]' const longerArray = longest([1, 2], [1, 2, 3]); // longerString is of type 'string' const longerString = longest("alice", "bob"); // Error! Numbers don't have a 'length' property const notOK = longest(10Argument of type '10' is not assignable to parameter of type '{ length: number; }'., 100);TryArgument of type '10' is not assignable to parameter of type '{ length: number; }'.
There are a interesting few things to note in this example.
We allowed TypeScript to infer the return type of longest
.
Return type inference also works on generic functions.
Because we constrained T
to { length: number }
, we were allowed to access the .length
property of the a
and b
parameters.
Without the type constraint, we wouldn't be able to access those properties because the values might have been some other type without a length property.
The types of longerArray
and longerString
were inferred based on the arguments.
Remember, generics are all about relating two or more values with the same type!
Finally, just as we'd like, the call to longest(10, 100)
is rejected because the number
type doesn't have a .length
property.
Working with Constrained Values
Here's a common error when working with generic constraints:
function minimumLength<T extends { length: number }>(obj: T, minimum: number): T { if (obj.length >= minimum) { return obj; } else { return { length: minimum };Type '{ length: number; }' is not assignable to type 'T'. } }TryType '{ length: number; }' is not assignable to type 'T'.
It might look like this function is OK - T
is constrained to { length: number }
, and the function either returns T
or a value matching that constraint.
The problem is that the function promises to return the same kind of object as was passed in, not just some object matching the constraint.
If this code were legal, you could write code that definitely wouldn't work:
// 'arr' gets value { length: 6 } const arr = minimumLength([1, 2, 3], 6); // and crashes here because arrays have // a 'slice' method, but not the returned object! console.log(arr.slice(0));Try
Specifying Type Arguments
TypeScript can usually infer the intended type arguments in a generic call, but not always. For example, let's say you wrote a function to combine two arrays:
function combine<T>(arr1: T[], arr2: T[]): T[] { return arr1.concat(arr2); }Try
Normally it would be an error to call this function with mismatched arrays:
const arr = combine([1, 2, 3], ["hello"Type 'string' is not assignable to type 'number'.]);Type 'string' is not assignable to type 'number'.
If you intended to do this, however, you could manually specify T
:
const arr = combine<string | number>([1, 2, 3], ["hello"]);
Guidelines for Writing Good Generic Functions
Writing generic functions is fun, and it can be easy to get carried away with type parameters. Having too many type parameters or using constraints where they aren't needed can make inference less successful, frustrating callers of your function.
Push Type Parameters Down
Here are two ways of writing a function that appear similar:
function firstElement1<T>(arr: T[]) { return arr[0]; } function firstElement2<T extends any[]>(arr: T) { return arr[0]; } // a: number (good) const a = firstElement1([1, 2, 3]); // b: any (bad) const b = firstElement2([1, 2, 3]);Try
These might seem identical at first glance, but firstElement1
is a much better way to write this function.
Its inferred return type is T
, but firstElement2
's inferred return type is any
because TypeScript has to resolve the arr[0]
expression using the constraint type, rather than "waiting" to resolve the element during a call.
Rule: When possible, use the type parameter itself rather than constraining it
Use Fewer Type Parameters
Here's another pair of similar functions:
function filter1<T>(arr: T[], func: (arg: T) => boolean): T[] { return arr.filter(func); } function filter2<T, F extends (arg: T) => boolean>(arr: T[], func: F): T[] { return arr.filter(func); }Try
We've created a type parameter F
that doesn't relate two values.
That's always a red flag, because it means callers wanting to specify type arguments have to manually specify an extra type argument for no reason.
F
doesn't do anything but make the function harder to read and reason about!
Rule: Always use as few type parameters as possible
Type Parameters Should Appear Twice
Sometimes we forget that function doesn't need to be generic:
function greet<S extends string>(s: S) { console.log("Hello, " + s); } greet("world");Try
We could just as easily have written a simpler version:
function greet(s: string) { console.log("Hello, " + s); }Try
Remember, type parameters are for relating the types of multiple values. If a type parameter is only used once in the function signature, it's not relating anything.
Rule: If a type parameter only appears in one location, strongly reconsider if you actually need it
Optional Parameters
Functions in JavaScript often take a variable number of arguments.
For example, the toFixed
method of number
takes an optional digit count:
function f(n: number) { console.log(n.toFixed()); // 0 arguments console.log(n.toFixed(3)); // 1 argument }Try
We can model this in TypeScript by marking the parameter as optional with ?
:
function f(x?: number) { // ... } f(); // OK f(10); // OKTry
Although the parameter is specified as type number
, the x
parameter will actually have the type number | undefined
because unspecified parameters in JavaScript get the value undefined
.
You can also provide a parameter default:
function f(x = 10) { // ... }Try
Now in the body of f
, x
will have type number
because any undefined
argument will be replaced with 10
.
Note that when a parameter is optional, callers can always pass undefined
, as this simply simualtes a "missing" argument:
declare function f(x?: number): void; // cut // All OK f(); f(10); f(undefined);Try
Optional Parameters in Callbacks
Once you've learned about optional parameters and function type expressions, it's very easy to make the following mistakes when writing functions that invoke callbacks:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i], i); } }Try
What people usually intend when writing index?
as an optional parameter is that they want both of these calls to be legal:
myForEach([1, 2, 3], a => console.log(a)); myForEach([1, 2, 3], (a, i) => console.log(a, i));
What this actually means is that callback
might get invoked with one argument.
In other words, the function definition says that the implementation might look like this:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) { // I don't feel like providing the index today callback(arr[i]); } }Try
In turn, TypeScript will enforce this meaning and issue errors that aren't really possible:
myForEach([1, 2, 3], (a, i) => { console.log(iObject is possibly 'undefined'..toFixed()); });TryObject is possibly 'undefined'.
In JavaScript, if you call a function with more arguments than there are parameters, the extra arguments are simply ignored. TypeScript behaves the same way. Functions with fewer parameters (of the same types) can always take the place of functions with more parameters.
When writing a function type for a callback, never write an optional parameter unless you intend to call the function without passing that argument
Function Overloads
Some JavaScript functions can be called in a variety of argument counts and types.
For example, you might write a function to produce a Date
that takes either a timestamp (one argument) or a month/day/year specification (three arguments).
In TypeScript, we can specify a function that can be called in different ways by writing overload signatures. To do this, write some number of function signatures (usually two or more), followed by the body of the function:
function makeDate(timestamp: number): Date; function makeDate(m: number, d: number, y: number): Date; function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { if (d !== undefined && y !== undefined) { return new Date(y, mOrTimestamp, d); } else { return new Date(mOrTimestamp); } } const d1 = makeDate(12345678); const d2 = makeDate(5, 5, 5); const d3 = makeDate(1, 3)No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.;TryNo overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
In this example, we wrote two overloads: one accepting one argument, and another accepting three arguments. These first two signatures are called the overload signatures.
Then, we wrote a function implementation with a compatible signature. Functions have an implementation signature, but this signature can't be called directly. Even though we wrote a function with two optional parameters after the required one, it can't be called with two parameters!
Overload Signatures and the Implementation Signature
This is a common source of confusion. Often people will write code like this and not understand why there is an error:
function fn(x: string): void; function fn() { // ... } // Expected to be able to call with zero arguments fn()Expected 1 arguments, but got 0.;TryExpected 1 arguments, but got 0.
Again, the signature used to write the function body can't be "seen" from the outside.
The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.
The implementation signature must also be compatible with the overload signatures. For example, these functions have errors because the implementation signature doesn't match the overloads in a correct way:
function fn(x: boolean): void; // Argument type isn't right function fnThis overload signature is not compatible with its implementation signature.(x: string): void; function fn(x: boolean) { }TryThis overload signature is not compatible with its implementation signature.
function fn(x: string): string; // Return type isn't right function fnThis overload signature is not compatible with its implementation signature.(x: number): boolean; function fn(x: string | number) { return "oops"; }TryThis overload signature is not compatible with its implementation signature.
Writing Good Overloads
Like generics, there are a few guidelines you should follow when using function overloads. Following these principles will make your function easier to call, easier to understand, and easier to implement.
Let's consider a function that returns the length of a string or an array:
function len(s: string): number; function len(arr: any[]): number; function len(x: any) { return x.length; }Try
This function is fine; we can invoke it with strings or arrays. However, we can't invoke it with a value that might be a string or an array, because TypeScript can only resolve a function call to a single overload:
len(""); // OK len([0]); // OK len(Math.random() > 0.5 ? "hello" : [0]Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type '"hello"' is not assignable to type 'any[]'.);TryArgument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'.Type '"hello"' is not assignable to type 'any[]'.
Because both overloads have the same argument count and same return type, we can instead write a non-overloaded version of the function:
function len(x: any[] | string) { return x.length; }Try
This is much better! Callers can invoke this with either sort of value, and as an added bonus, we don't have to figure out a correct implementation signature.
Always prefer parameters with union types instead of overloads when possible
Other Types to Know About
There are some additional types you'll want to recognize that appear often when working with function types. Like all types, you can use them everywhere, but these are especially relevant in the context of functions.
void
void
represents the return value of functions which don't return a value.
It's the inferred type any time a function doesn't have any return
statements, or doesn't return any explicit value from those return statements:
// The inferred return type is void function noop() { return; }Try
In JavaScript, a function that doesn't return any value will implicitly return the value undefined
.
However, void
and undefined
are not the same thing in TypeScript.
See the reference page Why void is a special type for a longer discussion about this.
void
is not the same asundefined
.
object
The special type object
refers to any value that isn't a primitive (string
, number
, boolean
, symbol
, null
, or undefined
).
This is different from the empty object type { }
, and also different from the global type Object
.
You can read the reference page about The global types for information on what Object
is for - long story short, don't ever use Object
.
object
is notObject
. Always useobject
!
Note that in JavaScript, function values are objects: They have properties, have Object.prototype
in their prototype chain, are instanceof Object
, you can call Object.keys
on them, and so on.
For this reason, function types are considered to be object
s in TypeScript.
unknown
The unknown
type represents any value.
This is similar to the any
type, but is safer because it's not legal to do anything with an unknown
value:
function f1(a: any) { a.b(); // OK } function f2(a: unknown) { aObject is of type 'unknown'..b(); }TryObject is of type 'unknown'.
This is useful when describing function types because you can describe functions that accept any value without having any
values in your function body.
Conversely, you can describe a function that returns a value of unknown type:
function safeParse(s: string): unknown { return JSON.parse(s); } // Need to be careful with 'obj'! const obj = safeParse(someRandomString);Try
never
Some functions never return a value:
function fail(msg: string): never { throw new Error(msg); }Try
The never
type represents values which are never observed.
In a return type, this means that the function throws an exception or terminates execution of the program.
never
also appears when TypeScript determines there's nothing left in a union.
function fn(x: string | number) { if (typeof x === "string") { // do something } else if (typeof x === "number") { // do something else } else { x; // has type 'never'! } }Try
Function
The global type Function
describes properties like bind
, call
, apply
, and others present on all function values in JavaScript.
It also has the special property that values of type Function
can always be called; these calls return any
:
function doSomething(f: Function) { f(1, 2, 3); }Try
This is an untyped function call and is generally best avoided because of the unsafe any
return type.
If need to accept an arbitrary function but don't intend to call it, the type () => void
is generally safer.
Rest Parameters and Arguments
Background reading: Rest Parameters and Spread Syntax
Rest Parameters
In addition to using optional parameters or overloads to make functions that can accept a variety of fixed argument counts, we can also define functions that take an unbounded number of arguments using rest parameters.
A rest parameter appears after all other parameters, and uses the ...
syntax:
function multiply(n: number, ...m: number[]) { return m.map(x => n * x); } // 'a' gets value [10, 20, 30, 40] const a = multiply(10, 1, 2, 3, 4);Try
In TypeScript, the type annotation on these parameters is implicitly any[]
instead of any
, and any type annotation given must be of the form Array<T>
or T[]
, or a tuple type (which we'll learn about later).
Rest Arguments
Conversely, we can provide a variable number of arguments from an array using the spread syntax.
For example, the push
method of arrays takes any number of arguments:
const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; arr1.push(...arr2);Try
Note that in general, TypeScript does not assume that arrays are immutable. This can lead to some surprising behavior:
// Inferred type is number[] -- "an array with zero or more numbers", // not specfically two numbers const args = [8, 5]; const angle = Math.atan2(...argsExpected 2 arguments, but got 0 or more.);TryExpected 2 arguments, but got 0 or more.
The best fix for this situation depends a bit on your code, but in general a const
context is the most straightforward solution:
// Inferred as 2-length tuple const args = [8, 5] as const; // OK const angle = Math.atan2(...args);Try
Parameter Destructuring
You can use parameter destructuring to conveniently unpack objects provided as an argument into one or more local variables in the function body. In JavaScript, it looks like this:
function sum({ a, b, c }) { console.log(a + b + c); } sum({ a: 10, b: 3, c: 9 });Try
The type annotation for the object goes after the destructuring syntax:
function sum({ a, b, c }: { a: number, b: number, c: number }) { console.log(a + b + c); }Try
This can look a bit verbose, but you can use a named type here as well:
// Same as prior example type ABC = { a: number, b: number, c: number }; function sum({ a, b, c }: ABC) { console.log(a + b + c); }Try
Assignability of Functions
Types from Extraction
TypeScript's type system is very powerful because it allows expressing types in terms of other types. Although the simplest form of this is generics, we actually have a wide variety of type operators available to us. It's also possible to express types in terms of values that we already have.
By combining various type operators, we can express complex operations and values in a succinct, maintainable way. In this chapter we'll cover ways to express a type in terms of an existing type or value.
The typeof
type operator
JavaScript already has a typeof
operator you can use in an expression context:
// Prints "string" console.log(typeof "Hello world");
TypeScript adds a typeof
operator you can use in a type context to refer to the type of a variable or property:
let s = "hello"; let n: typeof s; ▲let n: string
This isn't very useful for basic types, but combined with other type operators, you can use typeof
to conveniently express many patterns.
For an example, let's start by looking at the predefined type ReturnType<T>
.
It takes a function type and produces its return type:
type Predicate = (x: unknown) => boolean; type K = ReturnType<Predicate>; ▲type K = boolean
If we try to use ReturnType
on a function name, we see an instructive error:
function f() { return { x: 10, y: 3 }; } type P = ReturnType<f'f' refers to a value, but is being used as a type here.>;Try'f' refers to a value, but is being used as a type here.
Remember that values and types aren't the same thing.
To refer to the type that the value f
has, we use typeof
:
function f() { return { x: 10, y: 3 }; } type P = ReturnType<typeof f>; ▲type P = { x: number; y: number; }Try
Limitations
TypeScript intentionally limits the sorts of expressions you can use typeof
on.
Specifically, it's only legal to use typeof
on identifiers (i.e. variable names) or their properties.
This helps avoid the confusing trap of writing code you think is executing, but isn't:
// Meant to use = let x : msgbox(',' expected."Are you sure you want to continue?");',' expected.
The keyof
type operator
The keyof
operator takes a type and produces a string or numeric literal union of its keys:
type Point = { x: number, y: number }; type P = keyof Point; ▲type P = "x" | "y"
If the type has a string
or number
index signature, keyof
will return those types instead:
type Arrayish = { [n: number]: unknown }; type A = keyof Arrayish; ▲type A = number type Mapish = { [k: string]: boolean }; type M = keyof Mapish; ▲type M = string | numberTry
Note that in this example, M
is string | number
-- this is because JavaScript object keys are always coerced to a string, so obj[0]
is always the same as obj["0"]
.
keyof
types become especially useful when combined with mapped types, which we'll learn more about later.
Indexed Access Types
We can use typeof
to reference the type of a property of a value.
What if we want to reference the type of a property of a type instead?
We can use an indexed access type to look up a specific property on another type:
type Person = { age: number, name: string, alive: boolean }; type A = Person["age"]; ▲type A = number
The indexing type is itself a type, so we can use unions, keyof
, or other types entirely:
type I1 = Person["age" | "name"]; ▲type I1 = string | number type I2 = Person[keyof Person]; ▲type I2 = string | number | boolean type AliveOrName = "alive" | "name"; type I3 = Person[AliveOrName]; ▲type I3 = string | booleanTry
You'll even see an error if you try to index a property that doesn't exist:
type I1 = Person["alve"Property 'alve' does not exist on type 'Person'.];Property 'alve' does not exist on type 'Person'.
Another example of indexing with an arbitrary type is using number
to get the type of an array's elements.
We can combine this with typeof
to conveniently capture the element type of an array literal:
const MyArray = [ { name: "Alice", age: 15 }, { name: "Bob", age: 23 }, { name: "Eve", age: 38 } ]; type T = (typeof MyArray)[number]; ▲type T = { name: string; age: number; }Try
Types from Transformation
There are certain patterns that are very commonplace in JavaScript, like iterating over the keys of objects to create new ones, and returning different values based on the inputs given to us.
This idea of creating new values and types on the fly is somewhat untraditional in typed languages, but TypeScript provides some useful base constructs in the type system to accurately model that behavior, much in the same way that keyof
can be used to discuss the property names of objects, and indexed access types can be used to fetch values of a certain property name.
We'll quickly see that combined, these smaller constructs can be surprisingly powerful and can express many patterns in the JavaScript ecosystem.
Conditional Types
At the heart of most useful programs, we have to make decisions based on input. JavaScript programs are no different, but given the fact that values can be easily introspected, those decisions are also based on the types of the inputs. Conditional types help describe the relation between the types of inputs and outputs.
interface Animal { live(): void; } interface Dog extends Animal { woof(): void; } type Foo = Dog extends Animal ? number : string; ▲type Foo = number type Bar = RegExp extends Animal ? number : string; ▲type Bar = stringTry
Conditional types take a form that looks a little like conditional expresions (cond ? trueExpression : falseExpression
) in JavaScript:
SomeType extends OtherType ? TrueType : FalseType
When the type on the left of the extends
is assignable to the one on the right, then you'll get the type in the first branch (the "true" branch); otherwise you'll get the type in the latter branch (the "false" branch).
From the examples above, conditional types might not immediately seem useful - we can tell ourselves whether or not Dog extends Animal
and pick number
or string
!
But the power of conditional types comes from using them with generics.
For example, let's take the following createLabel
function:
interface IdLabel { id: number, /* some fields */ } interface NameLabel { name: string, /* other fields */ } function createLabel(id: number): IdLabel; function createLabel(name: string): NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel { throw "unimplemented"; }Try
These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:
- If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.
- We have to create three overloads: one for each case when we're sure of the type (one for
string
and one fornumber
), and one for the most general case (taking astring | number
). For every new typecreateLabel
can handle, the number of overloads grows exponentially.
Instead, we can encode that logic in a conditional type:
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
We can then use that conditional type to simplify out overloads down to a single function with no overloads.
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { throw "unimplemented" } let a = createLabel("typescript"); ▲let a: NameLabel let b = createLabel(2.8); ▲let b: IdLabel let c = createLabel(Math.random() ? "hello" : 42); ▲let c: IdLabel | NameLabelTry
Conditional Type Constraints
Often, the checks in a conditional type will provide us with some new information. Just like with narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constraint generics by the type we check against.
For example, let's take the following:
type MessageOf<T> = T["message"]Type '"message"' cannot be used to index type 'T'.;Type '"message"' cannot be used to index type 'T'.
In this example, TypeScript errors because T
isn't known to have a property called message
.
We could constrain T
, and TypeScript would no longer complain:
type MessageOf<T extends { message: unknown }> = T["message"]; interface Email { message: string; } interface Dog { bark(): void; } type EmailMessageContents = MessageOf<Email>; ▲type EmailMessageContents = stringTry
However, what if we wanted MessageOf
to take any type, and default to something like never
if a message
property isn't available?
We can do this by moving the constraint out and introducing a conditional type:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; interface Email { message: string } interface Dog { bark(): void } type EmailMessageContents = MessageOf<Email>; ▲type EmailMessageContents = string type DogMessageContents = MessageOf<Dog>; ▲type DogMessageContents = neverTry
Within the true branch, TypeScript knows that T
will have a message
property.
As another example, we could also write a type called Flatten
that flattens array types to their element types, but leaves them alone otherwise:
type Flatten<T> = T extends any[] ? T[number] : T // Extracts out the element type. type Str = Flatten<string[]>; ▲type Str = string // Leaves the type alone. type Num = Flatten<number>; ▲type Num = numberTry
When Flatten
is given an array type, it uses an indexed access with number
to fetch out string[]
's element type.
Otherwise, it just returns the type it was given.
Inferring Within Conditional Types
We just found ourselves using conditional types to apply constraints and then extract out types. This ends up being such a common operation that conditional types make it easier.
Conditional types provide us with a way to infer from types we compare against in the true branch using the infer
keyword.
For example, we could have inferred the element type in Flatten
instead of fetching it out "manually" with an indexed access type:
type Flatten<T> = T extends Array<infer U> ? U : T;
Here, we used the infer
keyword declaratively introduced a new generic type variable named U
instead of specifying how to retrieve the element type of T
.
Within the true branch
This frees us from having to think about how to dig through and probing apart the structure of the types we're interested.
We can write some useful helper type aliases using the infer
keyword.
For example, for simple cases, we can extract the return type out from function types:
type GetReturnType<T> = T extends (...args: never[]) => infer U ? U : never; type Foo = GetReturnType<() => number>; ▲type Foo = number type Bar = GetReturnType<(x: string) => string>; ▲type Bar = string type Baz = GetReturnType<(a: boolean, b: boolean) => boolean[]>; ▲type Baz = boolean[]Try
Distributive Conditional Types
When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:
type Foo<T> = T extends any ? T[] : never;
If we plug a union type into Foo
, then the conditional type will be applied to each member of that union.
type Foo<T> = T extends any ? T[] : never; type Bar = Foo<string | number>; ▲type Bar = string[] | number[]Try
What happens here is that Foo
distributes on
string | number
and maps over each member type of the union, to what is effectively
Foo<string> | Foo<number>
which leaves us with
string[] | number[]
Typically, distributivity is the desired behavior.
To avoid that behavior, you can surround each side of the extends
keyword with square brackets.
type Foo<T> = [T] extends [any] ? T[] : never; // 'Bar' is no longer a union. type Bar = Foo<string | number>; ▲type Bar = (string | number)[]Try
Classes
TypeScript offers full support for the class
keyword introduced in ES2015.
As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.
- Class Members
- Class Heritage
- Member Visibility
- Static Members
- Generic Classes
this
at Runtime in Classesthis
Types- Parameter Properties
- Class Expressions
abstract
Classes and Members- Relationships Between Classes
Class Members
Here's the most basic class - an empty one:
class Point { }Try
This class isn't very useful yet, so let's start adding some members.
Fields
A field declaration creates a public writeable property on a class:
class Point { x: number; y: number; } const pt = new Point(); pt.x = 0; pt.y = 0;Try
As with other locations, the type annotation is optional, but will be an implict any
if not specified.
Fields can also have initializers; these will run automatically when the class is instantiated:
class Point { x = 0; y = 0; } const pt = new Point(); // Prints 0, 0 console.log(`${pt.x}, ${pt.y}`);Try
Just like with const
, let
, and var
, the initializer of a class property will be used to infer its type:
const pt = new Point(); pt.xType '"0"' is not assignable to type 'number'. = "0";Type '"0"' is not assignable to type 'number'.
--strictPropertyInitialization
The strictPropertyInitialization
setting controls whether class fields need to be initialized in the constructor.
class BadGreeter { nameProperty 'name' has no initializer and is not definitely assigned in the constructor.: string; }TryProperty 'name' has no initializer and is not definitely assigned in the constructor.
class GoodGreeter { name: string; constructor() { this.name = "hello"; } }Try
Note that the field needs to be initialized in the constructor itself. TypeScript does not analyze methods you invoke from the constructor to detect initializations, because a derived class might override those methods and fail to initialize the members.
If you intend to definitely initialize a field through means other than the constructor (for example, maybe an external library is filling in part of your class for you), you can use the definite assignment assertion operator, !
:
class OKGreeter { // Not initialized, but no error name!: string; }Try
readonly
Fields may be prefixed with the readonly
modifier.
This prevents assignments to the field outside of the constructor.
class Greeter { readonly name: string = "world"; constructor(otherName?: string) { if (otherName !== undefined) { this.name = otherName; } } err() { this.nameCannot assign to 'name' because it is a read-only property. = "not ok"; } } const g = new Greeter(); g.nameCannot assign to 'name' because it is a read-only property. = "also not ok";Cannot assign to 'name' because it is a read-only property.TryCannot assign to 'name' because it is a read-only property.
Constructors
Background Reading: Constructor (MDN)
Class constructors are very similar to functions. You can add parameters with type annotations, default values, and overloads:
class Point { x: number; y: number; // Normal signature with defaults constructor(x = 0, y = 0) { this.x = x; this.y = y; } }Try
class Point { // Overloads constructor(x: number, y: string); constructor(s: string); constructor(xs: any, y?: any) { // TBD } }Try
There are just a few differences between class constructor signatures and function signatures:
- Constructors can't have type parameters - these belong on the outer class declaration, which we'll learn about later
- Constructors can't have return type annotations - the class instance type is always what's returned
Super Calls
Just as in JavaScript, if you have a base class, you'll need to call super();
in your constructor body before using any this.
members:
class Base { k = 4; } class Derived extends Base { constructor() { // Prints a wrong value in ES5; throws exception in ES6 console.log(this'super' must be called before accessing 'this' in the constructor of a derived class..k); super(); } }Try'super' must be called before accessing 'this' in the constructor of a derived class.
Forgetting to call super
is an easy mistake to make in JavaScript, but TypeScript will tell you when it's necessary.
Methods
A function property on a class is called a method. Methods can use all the same type annotations as functions and constructors:
class Point { x = 10; y = 10; scale(n: number): void { this.x *= n; this.y *= n; } }Try
Other than the standard type annotations, TypeScript doesn't add anything else new to methods.
Note that inside a method body, it is still mandatory to access fields and other methods via this.
.
An unqualified name in a method body will always refer to something in the enclosing scope:
let x: number = 0; class C { x: string = "hello"; m() { // This is trying to modify 'x' from line 1, not the class property xType '"world"' is not assignable to type 'number'. = "world"; } }TryType '"world"' is not assignable to type 'number'.
Getters / Setters
Classes can also have accessors:
class C { _length = 0; get length() { return this._length; } set length(value) { this._length = value; } }Try
Note that a field-backed get/set pair with no extra logic is very rarely useful in JavaScript. It's fine to expose public fields if you don't need to add additional logic during the get/set operations.
TypeScript has some special inference rules for accessors:
- If no
set
exists, the property is automaticallyreadonly
- The type of the setter parameter is inferred from the return type of the getter
- If the setter parameter has a type annotation, it must match the return type of the getter
- Getters and setters must have the same Member Visibility
It is not possible to have accessors with different types for getting and setting.
If you have a getter without a setter, the field is automatically readonly
Index Signatures
Classes can declare index signatures; these work the same as Index Signatures for other object types:
class MyClass { [s: string]: boolean | ((s: string) => boolean); check(s: string) { return this[s] as boolean; } }Try
Because the index signature type needs to also capture the types of methods, it's not easy to usefully use these types. Generally it's better to store indexed data in another place instead of on the class instance itself.
Class Heritage
Like other langauges with object-oriented features, classes in JavaScript can inherit from base classes.
implements
Clauses
You can use an implements
clause to check that a class satisfies a particular interface
.
An error will be issued if a class fails to correctly implement it:
interface Pingable { ping(): void; } class Sonar implements Pingable { ping() { console.log('ping!'); } } class BallClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'. implements Pingable { pong() { console.log('pong!'); } }TryClass 'Ball' incorrectly implements interface 'Pingable'.Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
Classes may also implement multiple interfaces, e.g. class C implements A, B {
.
Cautions
It's important to understand that an implements
clause is only a check that the class can be treated as the interface type.
It doesn't change the type of the class or its methods at all.
A common source of error is to assume that an implements
clause will change the class type - it doesn't!
interface Checkable { check(name: string): boolean; } class NameChecker implements Checkable { check(s) { // Notice no error here return s.toLowercse() === "ok"; ▲(parameter) s: any } }Try
In this example, we perhaps expected that s
's type would be influenced by the name: string
parameter of check
.
It is not - implements
clauses don't change how the class body is checked or its type inferred.
Similarly, implementing an interface with an optional property doesn't create that property:
interface A { x: number; y?: number; } class C implements A { x = 0; } const c = new C(); c.yProperty 'y' does not exist on type 'C'. = 10;TryProperty 'y' does not exist on type 'C'.
extends
Clauses
Classes may extend
from a base class.
A derived class has all the properties and methods of its base class, and also define additional members.
class Animal { move() { console.log("Moving along!"); } } class Dog extends Animal { woof(times: number) { for (let i = 0; i < times; i++) { console.log("woof!"); } } } const d = new Dog(); // Base class method d.move(); // Derived class method d.woof(3);Try
Overriding Methods
A derived class can also override a base class field or property.
You can use the super.
syntax to access base class methods.
Note that because JavaScript classes are a simple lookup object, there is no notion of a "super field".
TypeScript enforces that a derived class is always a subtype of its base class.
For example, here's a legal way to override a method:
class Base { greet() { console.log("Hello, world!"); } } class Derived extends Base { greet(name?: string) { if (name === undefined) { super.greet(); } else { console.log(`Hello, ${name.toUpperCase()}`); } } } const d = new Derived(); d.greet(); d.greet("reader");Try
It's important that a derived class follow its base class contract. Remember that it's very common (and always legal!) to refer to a derived class instance through a base class reference:
// Alias the derived instance through a base class reference const b: Base = d; // No problem b.greet();Try
What if Derived
didn't follow Base
's contract?
class Base { greet() { console.log("Hello, world!"); } } class Derived extends Base { // Make this parameter required greetProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.(name: string) { console.log(`Hello, ${name.toUpperCase()}`); } }TryProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.Type '(name: string) => void' is not assignable to type '() => void'.
If we compiled this code despite the error, this sample would then crash:
const b: Base = new Derived(); // Crashes because "name" will be undefined b.greet();Try
Initialization Order
The order that JavaScript classes initialize can be surprising in some cases. Let's consider this code:
class Base { name = "base"; constructor() { console.log("My name is " + name); } } class Derived extends Base { name = "derived"; } // Prints "base", not "derived" const d = new Derived();Try
What happened here?
The order of class initialization, as defined by JavaScript, is:
- The base class fields are initialized
- The base class constructor runs
- The derived class fields are initialized
- The derived class constructor runs
This means that the base class constructor saw its own value for name
during its own constructor, because the derived class field initializations hadn't run yet.
Inheriting Built-in Types
Note: If you don't plan to inherit from built-in types like
Array
,Error
,Map
, etc., you may skip this section
In ES2015, constructors which return an object implicitly substitute the value of this
for any callers of super(...)
.
It is necessary for generated constructor code to capture any potential return value of super(...)
and replace it with this
.
As a result, subclassing Error
, Array
, and others may no longer work as expected.
This is due to the fact that constructor functions for Error
, Array
, and the like use ECMAScript 6's new.target
to adjust the prototype chain;
however, there is no way to ensure a value for new.target
when invoking a constructor in ECMAScript 5.
Other downlevel compilers generally have the same limitation by default.
For a subclass like the following:
class FooError extends Error { constructor(m: string) { super(m); } sayHello() { return "hello " + this.message; } }Try
you may find that:
- methods may be
undefined
on objects returned by constructing these subclasses, so callingsayHello
will result in an error. instanceof
will be broken between instances of the subclass and their instances, so(new FooError()) instanceof FooError
will returnfalse
.
As a recommendation, you can manually adjust the prototype immediately after any super(...)
calls.
class FooError extends Error { constructor(m: string) { super(m); // Set the prototype explicitly. Object.setPrototypeOf(this, FooError.prototype); } sayHello() { return "hello " + this.message; } }Try
However, any subclass of FooError
will have to manually set the prototype as well.
For runtimes that don't support Object.setPrototypeOf
, you may instead be able to use __proto__
.
Unfortunately, these workarounds will not work on Internet Explorer 10 and prior.
One can manually copy methods from the prototype onto the instance itself (i.e. FooError.prototype
onto this
), but the prototype chain itself cannot be fixed.
Member Visibility
You can use TypeScript to control whether certain methods or properties are visible to code outside the class.
public
The default visibility of class members is public
.
A public
member can be accessed by anywhere:
class Greeter { public greet() { console.log("hi!"); } } const g = new Greeter(); g.greet();Try
Because public
is already the default visibility modifier, you don't ever need to write it on a class member, but might choose to do so for style/readability reasons.
protected
protected
members are only visible to subclasses of the class they're declared in.
class Greeter { public greet() { console.log("Hello, " + this.getName()); } protected getName() { return "hi"; } } class SpecialGreeter extends Greeter { public howdy() { // OK to access protected member here console.log("Howdy, " + this.getName()); } } const g = new SpecialGreeter(); g.greet(); // OK g.getNameProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.();TryProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Exposure of protected
members
Derived classes need to follow their base class contracts, but may choose to expose a more general type with more capabilities.
This includes making protected
members public
:
class Base { protected m = 10; } class Derived extends Base { // No modifier, so default is 'public' m = 15; } const d = new Derived(); console.log(d.m); // OKTry
Note that Derived
was already able to freely read and write m
, so this doesn't meaningfully alter the "security" of this situation.
The main thing to note here is that in the derived class, we need to be careful to repeat the protected
modifier if this exposure isn't intentional.
Cross-hierarchy protected
access
Different OOP languages disagree about whether it's legal to access a protected
member through a base class reference:
class Base { protected x: number = 1; } class Derived1 extends Base { protected x: number = 5; } class Derived2 extends Base { f1(other: Derived2) { other.x = 10; } f2(other: Base) { other.xProperty 'x' is protected and only accessible through an instance of class 'Derived2'. = 10; } }TryProperty 'x' is protected and only accessible through an instance of class 'Derived2'.
Java, for example, considers this to be legal. On the other hand, C# and C++ chose that this code should be illegal.
TypeScript sides with C# and C++ here, because accessing x
in Derived2
should only be legal from Derived2
's subclasses, and Derived1
isn't one of them.
Moreover, if accessing x
through a Derived2
reference is illegal (which it certainly should be!), then accessing it through a base class reference should never improve the situation.
See also Why Can’t I Access A Protected Member From A Derived Class? which explains more of C#'s reasoning.
private
private
is like protected
, but doesn't allow access to the member even from subclasses:
class Base { private x = 0; } const b = new Base(); // Can't access from outside the class console.log(b.xProperty 'x' is private and only accessible within class 'Base'.);TryProperty 'x' is private and only accessible within class 'Base'.
class Derived extends Base { showX() { // Can't access in subclasses console.log(this.xProperty 'x' is private and only accessible within class 'Base'.); } }TryProperty 'x' is private and only accessible within class 'Base'.
Because private
members aren't visible to derived classes, a derived class can't increase its visibility:
class Base { private x = 0; } class DerivedClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'. extends Base { x = 1; }TryClass 'Derived' incorrectly extends base class 'Base'.Property 'x' is private in type 'Base' but not in type 'Derived'.
Cross-instance private
access
Different OOP languages disagree about whether different instances of the same class may access each others' private
members.
While languages like Java, C#, C++, Swift, and PHP allow this, Ruby does not.
TypeScript does allow cross-instance private
access:
class A { private x = 10; public sameAs(other: A) { // No error return other.x === this.x; } }Try
Caveats
Like other aspects of TypeScript's type system, private
and protected
are only enforced during type checking.
This means that JavaScript runtime constructs like in
or simple property lookup can still access a private
or protected
member:
class MySafe { private secretKey = 12345; }Try
// In a JavaScript file... const s = new MySafe(); // Will print 12345 console.log(s.secretKey);Try
If you need to protect values in your class from malicious actors, you should use mechanisms that offer hard runtime privacy, such as closures, weak maps, or private fields.
Static Members
Classes may have static
members.
These members aren't associated with a particular instance of the class.
They can be accessed through the class constructor object itself:
class MyClass { static x = 0; static printX() { console.log(MyClass.x); } } console.log(MyClass.x); MyClass.printX();Try
Static members can also use the same public
, protected
, and private
visibility modifiers:
class MyClass { private static x = 0; } console.log(MyClass.xProperty 'x' is private and only accessible within class 'MyClass'.);TryProperty 'x' is private and only accessible within class 'MyClass'.
Static members are also inherited:
class Base { static getGreeting() { return "Hello world"; } } class Derived extends Base { myGreeting = Derived.getGreeting(); }Try
Special Static Names
It's generally not safe/possible to overwrite properties from the Function
prototype.
Because classes are themselves functions that can be invoked with new
, certain static
names can't be used.
Function properties like name
, length
, and call
aren't valid to define as static
members:
class S { static nameStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'. = "S!"; }TryStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
Why No Static Classes?
TypeScript (and JavaScript) don't have a construct called static class
the same way C# and Java do.
Those constructs only exist because those languages force all data and functions to be inside a class; because that restriction doesn't exist in TypeScript, there's no need for them. A class with only a single instance is typically just represented as a normal object in JavaScript/TypeScript.
For example, we don't need a "static class" syntax in TypeScript because a regular object (or even top-level function) will do the job just as well:
// Unnecessary "static" class class MyStaticClass { static doSomething() { } } // Preferred (alternative 1) function doSomething() { } // Preferred (alternative 2) const MyHelperObject = { dosomething() { } }Try
Generic Classes
Classes, much like interfaces, can be generic.
When a generic class is instantiated with new
, its type parameters are inferred the same way as in a function call:
class Box<T> { contents: T; constructor(value: T) { this.contents = value; } } const b = new Box("hello!"); ▲const b: Box<string>Try
Classes can use generic constraints and defaults the same way as interfaces.
Type Parameters in Static Members
This code isn't legal, and it may not be obvious why:
class Box<T> { static defaultValue: TStatic members cannot reference class type parameters.; }TryStatic members cannot reference class type parameters.
Remember that types are always fully erased!
At runtime, there's only one Box.defaultValue
property slot.
This means that setting Box<string>.defaultValue
(if that were possible) would also change Box<number>.defaultValue
- not good.
The static
members of a generic class can never refer to the class's type parameters.
this
at Runtime in Classes
It's important to remember that TypeScript doesn't change the runtime behavior of JavaScript, and that JavaScript is somewhat famous for having some peculiar runtime behaviors.
JavaScript's handling of this
is indeed unusual:
class MyClass { name = "MyClass"; getName() { return this.name; } } const c = new MyClass(); const obj = { name: "obj", getName: c.getName }; // Prints "obj", not "MyClass" console.log(obj.getName());Try
Long story short, by default, the value of this
inside a function depends on how the function was called.
In this example, because the function was called through the obj
reference, its value of this
was obj
rather than the class instance.
This is rarely what you want to happen! TypeScript provides some ways to mitigate or prevent this kind of error.
Arrow Functions
If you have a function that will often be called in a way that loses its this
context, it can make sense to use an arrow function property instead of a method definition:
class MyClass { name = "MyClass"; getName = () => { return this.name; } } const c = new MyClass(); const g = c.getName; // Prints "MyClass" instead of crashing console.log(g());Try
This has some trade-offs:
- The
this
value is guaranteed to be correct at runtime, even for code not checked with TypeScript - This will use more memory, because each class instance will have its own copy of each function defined this way
- You can't use
super.getName
in a derived class, because there's no entry in the prototype chain to fetch the base class method from
this
parameters
In a method or function definition, an initial parameter named this
has special meaning in TypeScript.
These parameters are erased during compilation:
// TypeScript input with 'this' parameter function fn(this: SomeType, x: number) { /* ... */ }Try
// JavaScript output function fn(x) { /* ... */ }Try
TypeScript checks that calling a function with a this
parameter is done so with a correct context.
Instead of using an arrow function, we can add a this
parameter to method definitions to statically enforce that the method is called correctly:
class MyClass { name = "MyClass"; getName(this: MyClass) { return this.name; } } const c = new MyClass(); // OK c.getName(); // Error, would crash const g = c.getName; console.log(g()The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.);TryThe 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
This method takes the opposite trade-offs of the arrow function approach:
- JavaScript callers might still use the class method incorrectly without realizing it
- Only one function per class definition gets allocated, rather than one per class instance
- Base method definitions can still be called via
super.
this
Types
In classes, a special type called this
refers dynamically to the type of the current class.
Let's see how this is useful:
class Box { contents: string = ""; set(value: string) { ▲(method) Box.set(value: string): this this.contents = value; return this; } }Try
Here, TypeScript inferred the return type of set
to be this
, rather than Box
.
Now let's make a subclass of Box
:
class ClearableBox extends Box { clear() { this.contents = ""; } } const a = new ClearableBox(); const b = a.set("hello"); ▲const b: ClearableBoxTry
You can also use this
in a parameter type annotation:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; } }Try
This is different from writing other: Box
-- if you have a derived class, its sameAs
method will now only accept other instances of that same derived class:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; } } class DerivedBox extends Box { otherContent: string = "?"; } const base = new Box(); const derived = new DerivedBox(); derived.sameAs(baseArgument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.);TryArgument of type 'Box' is not assignable to parameter of type 'DerivedBox'.Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
Parameter Properties
TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value.
These are called parameter properties and are created by prefixing a constructor argument with one of the visibility modifiers public
, private
, protected
, or readonly
.
The resulting field gets those modifier(s):
class A { constructor (public readonly x: number, protected y: number, private z: number) { // No body necessary } } const a = new A(1, 2, 3); console.log(a.x); ▲(property) A.x: number console.log(a.zProperty 'z' is private and only accessible within class 'A'.);TryProperty 'z' is private and only accessible within class 'A'.
Class Expressions
Class expressions are very similar to class declarations. The only real difference is that class expressions don't need a name, though we can refer to them via whatever identifier they ended up bound to:
const someClass = class<T> { content: T; constructor(value: T) { this.content = value; } } const m = new someClass("Hello, world"); ▲const m: someClass<string>Try
abstract
Classes and Members
Classes, methods, and fields in TypeScript may be abstract.
An abstract method or abstract field is one that hasn't had an implementation provided. These members must exist inside an abstract class, which cannot be directly instantiated.
The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract members. When a class doesn't have any abstract members, it is said to be concrete.
Let's look at an example
abstract class Base { abstract getName(): string; printName() { console.log("Hello, " + this.getName()); } } const b = new Base()Cannot create an instance of an abstract class.;TryCannot create an instance of an abstract class.
We can't instantiate Base
with new
because it's abstract.
Instead, we need to make a derived class and implement the abstract members:
class Derived extends Base { getName() { return "world"; } } const d = new Derived(); d.printName();Try
Notice that if we forget to implement the base class's abstract members, we'll get an error:
class DerivedNon-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'. extends Base { // forgot to do anything }TryNon-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
Abstract Construct Signatures
Sometimes you want to accept some class constructor function that produces an instance of a class which derives from some abstract class.
For example, you might want to write this code:
function greet(ctor: typeof Base) { const instance = new ctor()Cannot create an instance of an abstract class.; instance.printName(); }TryCannot create an instance of an abstract class.
TypeScript is correctly telling you that you're trying to instantiate an abstract class.
After all, given the definition of greet
, it's perfectly legal to write this code, which would end up constructing an abstract class:
// Bad! greet(Base);
Instead, you want to write a function that accepts something with a construct signature:
function greet(ctor: new() => Base) { const instance = new ctor(); instance.printName(); } greet(Derived); greet(BaseArgument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.);TryArgument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.Cannot assign an abstract constructor type to a non-abstract constructor type.
Now TypeScript correctly tells you about which class constructor functions can be invoked - Derived
can because it's concrete, but Base
cannot.
Relationships Between Classes
In most cases, classes in TypeScript are compared structurally, the same as other types.
For example, these two classes can be used in place of each other because they're identical:
class Point1 { x = 0; y = 0; } class Point2 { x = 0; y = 0; } // OK const p: Point1 = new Point2();Try
Similarly, subtype relationships between classes exist even if there's no explicit inheritance:
class Person { name: string; age: number; } class Employee { name: string; age: number; salary: number; } // OK const p: Person = new Employee();Try
This sounds straightforward, but there are a few cases that seem stranger than others.
Empty classes have no members. In a structural type system, a type with no members is generally a supertype of anything else. So if you write an empty class (don't!), anything can be used in place of it:
class Empty { } function fn(x: Empty) { // can't do anything with 'x', so I won't } // All OK! fn(window); fn({ }); fn(fn);Try