TypeScript is a programming language made by Microsoft that is superset of JavaScript. The reason that TypeScript exists is to make complex JavaScript projects more maintainable and less error-prone by introducing a static and strong type system. Essentially, it just gives developers a lot of quality-of-life improvements over JavaScript.
Note: TypeScript gets compiled (or more precisely, âtranspiledâ) to JavaScript in the end. This is not new, languages like CoffeeScript, Dart, Scala, etc. can all have JavaScript as what we call a compilation target.
I learned TypeScript from the official docs and from the âProgramming TypeScriptâ textbook by Boris Cherny.
Why TypeScript is loved
- Your IDE/editor gets more information about your code and give you very helpful intellisense and code-completion that is not possible with JavaScript. This reason alone, in my experience, pretty much negates any loss in developer velocity from using TypeScript over JavaScript.
- Many errors will surface as youâre developing rather than after your code is deployed to production and angry customers complain to you.
- Types serve as a useful concise form of documentation for how your functions are to be used and what fields an object contain.
- Complex objects are much less unpleasant to work with. Youâll know what its shape is (basically what properties it has and what its nested objects look like), what properties are compulsory or optional and youâll actually know when youâve mispelt a property name.
- Youâll almost never have
cannot read property '...' of undefined
again.
Setup
Install Node.js, npm
or yarn
. Then, install tsc
, the open-source typescript compiler, as a dev dependency in a javascript project:
tsconfig.json
Every typescript project should have a tsconfig.json
file at the root of the project directory. It tells tsc
which files to compile, where to dump the resulting javascript, and so on. A basic config looks like this (but there are so many more options):
Alternatively, you can generate a tsconfig.json
with tsc --init
.
Some recommended flags include:
noImplicitThis
â forces a type to be explicitly assinged tothis
inside functions. See TypeScript this.noImplicitOverride
â you must always use theoverride
modifier for method overriding.noFallthroughCasesInSwitch
â every case must eitherbreak
orreturn
.esModuleInterop
â makes it more smooth to consume JavaScript modules that use CommonJS, AMD or other module systems.
Typing
Broadly speaking, in programming languages, a type is a set of values, plus the properties/methods available to them.
Assigning Types
Assigning types is straightforward in TypeScript, you just postfix a variable or parameter with a colon and a type.
Defining Types
Remember, types are just sets of values. When you define a type, you are just defining a set of values. The following are all examples of custom types you can define:
An important thing to understand about TypeScript (and many other statically-typed languages) is that it has separate namespaces for values and types. This means that in the following example, a variable identifier with the same name as a type alias are not in conflict. TypeScript can infer if you meant the value or the type.
Type Alias
You can declare type aliases in a very similar way as to how you define variables. Type aliases are block-scoped, just like local variables.
Interface
Interfaces are basically an alternative to type aliases, but are mostly better suited for defining object shapes. You canât use &
or |
for interfaces, but you can use extends
.
Classes vs. Interfaces
Using interface
does not actually generate any javascript code when transpiled. Using class
, however, will generate JavaScript code, which enables instanceof
to work at runtime. A class Foo { ... }
definition actually creates a value Foo
that can be used in expressions, and a type Foo
that can be used as a type.
Interfaces donât let you use access modifiers. You canât supply implementations either.
Keep these critical differences in mind when deciding between class
or interface
.
Assignability
Assignability is about what the rules are around an assignment like this: const a: A = b;
, where b
is of type B
. For assignment to be valid, it must be the case that:
B
is a subtype ofA
(basically that ),- ⌠or
B
isany
. Note: this rule only exists to make it easier to interoperate with javascript code.
Union and Intersection
Again, types are just sets of values. To expand the size of a set, you can union it with other sets, and to narrow the size of a set, you can intersect it other sets. In TypeScript, we use |
to union two types and &
to intersect two types.
To make a type nullable, you can union it with null
like this: type MiddleName = string | null;
TypeScript Built-In Types
TypeScript introduces some new built-in data types that arenât present in JavaScript. These are: any
, unknown
, void
, never
.
any
The any
type represents the set of all values. You can assign a variable of type any
to a number, a string, a WebServer object, etc. Only use any
as a last resort â always prefer assigning the most specific type that you can. Often, people treat any
as a way to âoptâ out of TypeScript for a small part of the code. When something is any
, you are free to do erroneous things on it such as invoking methods on it that donât exist, using in arithmetic expressions, accessing undefined properties, etc.
unknown
The unknown
type represents the set of all values, just like any
. The difference is that TypeScript does not allow you to use an unknown
value until you perform type checks
or refinement to narrow down what specific type the unknown
value is. For this reason, unknown
is considered the type-safe version of any
.
A useful way to think about
any
andunknown
is:any
means âI donât careâ,unknown
means âI donât know (yet)â.
Object Shape
An important part of using TypeScript effectively is in defining the shape of object values, or in other words, what properties they have and which ones must be assigned a value or are optional.
Note: the built-in object
type is the set of all objects, whether itâs {}
, {hello: "world"}
, []
, new Date()
, etc. Itâs only slightly more restrictive than any
.
Optional Properties
By default, all properties are treated as compulsory. To allow an object to not define a property, just postfix the property name with ?
.
Immutable Properties
You can make properties immutable by prefixing the property name with the readonly
property. Itâs like as if you declared a property as const
, so once itâs assigned for the first time, it cannot be reassigned.
Index Signatures
When you want the flexibility for an object to contain more arbitrary properties with a certain key-value pair type, you can use the index signature syntax, [key: K]: V
, where key
can be any name you choose.
Indexed Access Types
See indexed access types. When you define a type for an object shape, if you want to access a nested part of that shape as a type, you can just use the subscript operator []
.
Nope, you canât use the .
operator as if you were accessing object properties.
keyof
The keyof
unary operator evaluates to the union of a typeâs keys.
This is helpful for writing getter functions that retrieve values nested in an object:
Arrays
To denote a type as an array of items of type T
, you can do T[]
or Array<T>
(theyâre exactly the same).
In general, try to keep arrays homogenous, that is, of a singular type, otherwise youâd have to do type-checking before TypeScript can let you use the items of the array.
Arrays are always mutable by default in TypeScript, that is, you can reassign the value at any index and invoke methods that make in-place modifications like push
or reverse
. To make them immutable, you would prefix them with the readonly
modifier.
Tuples
Remember, tuples are just fixed-length arrays. In typescript, we define a tuple by specifying the type of each item like: [T1, T2, ...]
:
You can also make use of the rest operator, ...
, to allow for tuples of arbitrary lengths.
Caveat: tuples are not immutable by default.
Unlike in other languages like Python, the items of a tuple in TypeScript can be mutated, that is, reassigned after definition. To make tuples immutable, you would do the same thing as you would for making arrays immutable: prefix it with the readonly
modifier.
Enums
Enums are data types that have a fixed set of constant values. Theyâre a great way to group together a lot of related constants. Note: JavaScript doesnât have enums.
In the example above, every enum value gets implicitly assigned a counter value starting from 0. Itâs equivalent to doing the following:
You can also map enum keys to string values instead of integers.
Caveats
One annoying issue is that you can freely assign numbers to enum types that are clearly outside the bounds of the enum values.
In general, the official docs advise you to avoid enums unless they help significantly with readability. Alternatives to enums include string literals, eg. type Theme = "Light" | "Dark" | ...
, or object literals, eg. const Theme = { Light: "Light", Dark: "Dark", ... }
.
Type Inference
You donât have to always supply an explicit type. Often, thereâll be enough context for TypeScript to figure it out without ambiguity. In general, we prefer type inference over explict assigning types to varaibles/parameters/etc. for conciseness.
This extends to functions as well, meaning that often you wonât have to specify the return value.
Type Widening/Narrowing
An important implicit rule in TypeScript is that when you let type inference happen for const
variables, TypeScript will assign it the narrowest type possible since it knows that a const
variable cannot possibly take any other value after its defined. Otherwise, TypeScript will infer the type to be wider than it might be.
typeof and instanceof
Although type checking is done for you statically, there are times when you must perform run-time type checks such as when youâre fetching external data. In these times, rely on JavaScriptâs operators: typeof
and instanceof
.
- Use the
instanceof
binary operator to check some value is of a custom type, or a complex built-in type likeRegExp
.- Note that
val instanceof T
works by checking ifT
exists anywhere alongval
âs prototype chain. This is why you get unintuitive results when you useinstanceof
on primitive types. For example,42 instanceof Number
isfalse
, butnew Number(42) instanceof Number
istrue
.
- Note that
- Use the
typeof
unary operator to check some value is some built-in primitive type such asundefined
,number
,string
,boolean
, etc.
Type Assertions
When youâre confident that some value should be a certain type but TypeScript isnât, you can make a type assertion with the as
keyword.
You can also make type assertions by prefixing an expression with <T>
, eg. <Person>person
which is exactly the same as person as Person
.
Aim to minimise your usage of type assertions like above. Theyâre considered âescape hatchesâ from the language and can prevent you from maximising the benefits of using a type system.
Const Assertion
Use as const
to tell TypeScript to infer the value to its narrowest possible type.
Nonnull Assertion
When youâre confident a value is not null, you can postfix that value with !
to assure TypeScript. If you cannot be confident, then just use a standard null-check: if (_ === null) ...
.
Refinement
TypeScriptâs static analysis can handle refinement where, based on the control flow logic, TypeScript can narrow the type of the variable. Refinement can happen when you use if
, the optional chaining operator ?.
, ||
, switch
, typeof
, instanceof
, in
, etc.
Refinement works with unioned objects, but itâs best to use unique strings to help TypeScript infer types properly.
This kind of type refinement is very useful when working with Redux reducers.
Type Guards
Refinement doesnât work as expected when you use a function to do the type-checking. Any type-checking only contributes to refinement if itâs in the same scope.
To fix this, youâd need to define a type guard which is a predicate function that confirms an argument is a given type. It looks like this:
Variance
Itâs useful to think of types as just sets. When is a subtype of , it is basically just saying that .
Variance, in programming language theory, is how subtyping works for generic types. It is concerned about the idea of whether a generic type like List<Cat>
is a subtype of List<Animal>
.
There are 4 kinds of variance:
- Invariance â says that
List<T>
is not a subtype ofList<U>
regardless of whetherT extends U
. - Covariance â says that
List<T>
is a subtype ofList<U>
ifT extends U
. - Contravariance â says that
List<T>
is a subtype ofList<U>
ifU extends T
, ie. going the other way of covariance. - Bivariance â says that
List<T>
is a subtype ofList<U>
if eitherT extends U
orU extends T
.
Every languageâs type system has different rules around variance. As a programming language designer, if you were to allow covariance or contravariance over invariance, then youâre allowing for greater flexibility in the type system, but it exposes programmers to greater risk of runtime type errors.
TypeScript tends to be more relaxed by allowing functions to take in covariant arguments. For example, you can pass an argument so long as it is a subtype of the expected parameter, ie. covariant to the expected parameter, but this makes it possible to create run-time type errors like this:
Functions
Typing Function Declarations To type a function declaration, you just assign types for each parameter it takes in and then specify the return type by postfixing the parameter list with a colon and a type.
Typing Function Expressions or Arrow Functions
What if you want to specify the type of a callback rather than a function declaration? You would use the syntax: (param: Type, ...) => RetType
. Although the syntax is inspired by arrow functions, it is not actually defining a function.
Optional & Default Parameters
Just like how you can make object properties optional and tuple items optional, you can make function parameters optional by postfixing the parameter name with a ?
. Alternatively, you can set a default value for a parameter by assigning a value directly after the parameter name, which is pretty much the same as making it optional.
Variadic Functions
A variadic function is just one that takes in an arbitrary number of arguments. The vast majority of functions take in a fixed list of parameters, we call these âfixed-arityâ functions. Normally in JavaScript, defining a variadic function requires you to make use of the implicit arguments
array in the function body. In TypeScript, itâs way more intuitive and can be done in a type-safe way with the rest operator, ...
.
this
In JavaScript, this
is a nightmare for most programmers to work with because its value is different depending on how it is called. In TypeScript, you can minimise surprises around the value of this
by assigning a type to it as the first function parameter.
To illustrate the problem:
The âsolutionâ is to assign a type for this
so that the developer is warned when this
takes on the wrong type when they invoke a function that uses it.
Generators
See generators. In JavaScript, you can create a generator function by postfixing function
with an asterisk, *
. Note: you cannot define arrow functions as generator functions (at least as of 2022âs ES standard).
TypeScript automatically infers the return type of the generator to be IterableIterator<number>
. To assign an explicit type for what gets yielded, do it the same way that youâd specify the return value, but wrap it around with IterableIterator
.
Iterators
See iterators. In JavaScript, an iterable is an object containing the Symbol.iterator
property with the value being a function that returns an iterator (which can be done by defining Symbol.iterator to be a generator function, which always returns an iterator). An iterator is an object that defines a next
method which returns an object of shape: { value: any, done: boolean }
.
An object can be both an iterable and an iterator at the same time. When you invoke a generator function, for example, you get an object
of type IterableIterator
which is both, meaning it has a Symbol.iterator
property, whose value is a function that returns an iterator, and the next
method.
Note: the syntax for defining Symbol.iterator
as a generator function might seem strange. See this post for clarifications. As for the square brackets around Symbol.iterator
, itâs called the computed property name syntax, introduced in ES6.
Function Overloading
You can define a function type that actually consists of multiple function signatures. See the Function Overloads.
Generic Functions
See generics. In TypeScript, you can define generic functions by specifying a comma-separated list of generic type parameters in angle brackets <>
right before the parameter list of function. You would use generic functions if you wanted a function to be reusable across multiple types without giving up type safety by resorting to any
.
To make regular function declarations generic, you also place the generic type parameters between angle brackets right before the parameter list:
Bind on Reference
In the above example, T
gets bound when the function gets invoked, but you could also bind T
whenever the type alias gets referenced by placing the generic type parameters after the type name instead of before the function parameter list:
Bounded Polymorphism
Sometimes, saying that a generic function takes in type parameter of T
is too permissive. Instead, we might want T
to be a subtype of U
, that is, we should accept type parameters that are âat leastâ U
. This is called bounded polymorphism or constrained genericity.
Object-Oriented Programming
See Object Oriented Programming.
Access Modifiers
See encapsulation. TypeScript offers 3 access modifiers, which can be prefixed to any class field declaration:
private
.protected
(which makes a member accessible to subclasses as well).public
. If no access modifier is specified, then fields arepublic
by default, unlike most languages which default toprivate
.
When prefixing a constructorâs parameter with an access modifier, itâll declare the field and assign the given value implicitly.
Inheritance
See inheritance. In TypeScript, inheritance works in the same way and uses the same syntax as JavaScriptâs inheritance.
Method Overriding
See method overriding. By default, every method is âvirtualâ, so you can override them all. To override a method in TypeScript, just copy the method signature and supply the new method body. As good practice, use the optional override
modifier so that youâre warned when youâve got the base classâ method signature wrong.
Abstract Classes
See abstract classes. To make a class abstract, just prefix it with the abstract
keyword.
Abstract Methods
See abstract methods. Abstract methods must be inside abstract classes. To make a method abstract, use the abstract
modifier, explicitly type the method signature and do not provide a body.
Generic Types in Classes/Interfaces
You can set class-scoped or interface-scoped generic type parameters:
Modules
See JavaScript modules. With TypeScript, you can additionally import/export type aliases and interfaces.
Note: in import statements, you donât need to specify the .ts
file extension. This means you can easily import type declaration files with the extensionless name.
In thing.ts
:
In main.ts
:
Error Handling
See JavaScript error handling. TypeScript doesnât introduce any new syntax for error handling over JavaScript, but the type system allows for streamlining how errors are treated in a project by developers.
Ways of Error Handling
There are 4 common patterns for handling errors in TypeScript projects, which are also mostly applicable to non-TypeScript projects:
- Just return
null
. This reveals the least information in the event of an error, but itâs the easiest to do. Constant null-checking is required throughout the code however, which can be laborious and verbose. - Throw an exception.
When an exception is thrown, it must be caught by the caller in a try-catch block (or a
catch
callback if using promises) otherwise a full crash occurs. Making and throwing custom subclasses ofError
would offer specific information to help with debugging and informing the user about the problem. The main issue is that itâs hard to enforce that programmers write the error-handling try-catch logic when theyâre lazy. - Return exceptions (rather than throw them).
This means a function will specify in its return type a union of the expected return type and the error classes that it could throw, such as in the following:
By putting the error as part of the return type, the user of the function is unlikely to ignore error cases.
The idea here is very similar in spirit to Javaâs
throws
keyword. The downside to this approach is that itâll lead to more verbose function signatures, especially if errors are simply âbubbledâ up the call stack. - Define and use the
Option
type. The idea comes from languages like Rust and Haskell. See Rustâs documentation onstd::option
.
Utility Types
TypeScript gives you a bunch of very useful built-in utility types that you can use to make working with complex types a breeze đŹď¸.
Mapping Types
Here are some of the most useful utility types for sourcing types from other types:
Partial<T>
â T, but every property is optional.Omit<T, Keys>
â T, but without the property inKeys
, which is a union of string property names.Pick<T, Keys>
â a type with propertiesKeys
, a union of string property names, sourced fromT
.Readonly<T>
â T, but every property is read-only.
Usage examples:
Note: behind the scenes, utility types such as the ones above are realised through âmapped typesâ.
Conditional Types
Here are some of the most useful utility types that leverage conditional typing, a TypeScript innovation.
Exclude<T, U>
â removes values in the setU
from the setT
.Extract<T, U>
â picks out elements inU
that are inT
.NonNullable<T>
â excludesnull
from the setT
.ReturnType<F>
â the return type of a functionâs typed signature.
Note: just like how you can use the ternary operator, (condition) ? expr1 : expr2
for conditional expression evaluation, you can use the ternary operator for conditional type evalution. This is whatâs used to implement those conditional utility types above.
Asynchronous Programming
See JavaScript asynchronous programming.
JavaScript Interoperability
An excellent reason to adopt TypeScript is that you donât have to rewrite your JavaScript codebase to begin benefiting from a type system.
Type Declaration Files
A type declaration file, which goes with the extension .d.ts
, associates types to JavaScript code. Itâs a file consisting only of type-level code, meaning you canât use expressions in there (which means no function implementations, variables, class implementations, etc. can be defined within). As a very loose analogy, .d.ts
files are kind of like the .h
header files in C or C++.
If you have a hello-world.js
file, then the type declaration file must have the name, hello-world.d.ts
.
A type declaration is a way to tell TypeScript, âThere exists this thing thatâs defined in JavaScript, and Iâm going to describe it to you.â (Programming TypeScript).
NPM packages that once were intended only for JavaScript developers (eg. jQuery) can be made consumable by TypeScript developers by having these type declaration files. As a TypeScript dev, youâd be able to use pure JS libraries as if they were written in TypeScript.
When type declarations donât ship with an NPM package, they can usually be install individually in the @types organisation on npm. The type declarations in DefinitelyTyped, a big community effort to bring types to popular JS libraries, are automatically published to the @types organisation.
Eg. to bring jQuery into a TypeScript frontend project, youâd do: