TypeScript Fundamentals
Published Jul 7, 2021
Table of Contents
- Youāre Already Using TypeScript
- Why Should You Use TypeScript?
- Runtime and Compile Time
- Gradual Adoption
- TypeScript Introduction Summary
- JavaScript Types
- TypeScript Playground
- Type Inference
- Type Annotations
- Primitive Types
- TypeScript Types
- any
- unknown
- void
- never
- Array Types
- Function Types
- Function Overloads
- Object Types
- Type Aliases
- Interfaces
- Type Aliases or Interfaces?
- Union Types
- Discriminated Unions
- Intersection Types
- Type Assertion
- Type Assertion Using
!
- Type Assertion Conversion
- Literal Types
- Literal Inference
- Object Index Signatures
- Type Narrowing
- Type Guards
- Type Predicates
- Generics
- Generic Interface
- Generic Constraints
- Generic Constraints Using Type Parameters
- Enums
- Tuple
- Classes
- Decorators
- Set Up TypeScript
- Reading Type Definitions
- Reading TypeScript Errors
- Dealing With Untyped Libraries
- Generate Types
- Conclusion
Youāre Already Using TypeScript
If youāre a web developer, youāre probably using Visual Studio Code (not to be confused with Visual Studio) and if youāre not at least consider it.
Have you ever asked yourself how code completion in your editor works?
const pokemon = ['Bulbasaur', 'Charmander', 'Squirtle']
The editor knows that pokemon
is an array of strings.
Since pokemon
is an array the editor intellisense is smart enough to only show us array methods.
Imagine not having this feature. You would have to remember and look every method up on the MDN Web Docs.
If we look at the map
array method we can see itās call signature and description in the tooltip.
This is JavaScript ā yet itās using TypeScript under the hood.
TypeScript isnāt just types ā itās popular because this is the kind of developer experience that every other developer is used to in other languages.
We can add @ts-check
at the top of a JavaScript file to enable type checking in JavaScript.
// @ts-check
const pokemon = ['Bulbasaur', 'Charmander', 'Squirtle']
pokemon.push(1) // oops! š©
The pokemon
array contains only strings, so we get a type error when trying to assign number
to string
.
The VS Code editor uses the TypeScript language server under the hood.
If we look at the built-in VS Code extensions, we can notice some usual suspects. Look, thereās Emmet. š
The language server provides us with sophisticated features such as code completion, refactoring, syntax highlighting, and error and warnings.
The Language Server Protocol allows for decoupling language services from the editor so that the services may be contained within a general-purpose language server. Any editor can inherit sophisticated support for many different languages by making use of existing language servers. Similarly, a programmer involved with the development of a new programming language can make services for that language available to existing editing tools. ā Language Server Protocol (Wikipedia).
Visual Studio Code and TypeScript are made by Microsoft, so that explains the tight integration. This doesnāt mean youāre left in the dark if youāre using another editor like WebStorm or Vim. The TypeScript language server is available as a plugin for those editors.
Your editor already has some great features that get enhanced by TypeScript:
- Auto imports (as you type imports get added)
- Code navigation (definitions, lookup)
- Rename (rename symbols across file)
- Refactoring (extracting code to functions)
- Quick fixes (suggested edits like fixing a mispelled property name)
- Code suggestions (for example converting
.then
to useasync
andawait
)
You can learn more about these features in-depth if you read the documentation for the JavaScript language.
Why Should You Use TypeScript?
When writing JavaScript we donāt get a lot of information before we run our code.
Thereās no way for us to know if the JavaScript code has errors until we see the result on the page and go back to our code editor to fix the mistake and rerun the code.
const pikachu = {
name: 'Pikachu',
weight: 60
}
pikachu.weigth // oops! š©
To prove my point Iām sure you barely noticed the mistake and had to look at what it was. š
The editor didnāt warn us about mispelling weight
.
Imagine the same scenario with an API where you pass wrong arguments to a method ā you just hope it works.
const pikachu = {
name: 'Pikachu',
weight: 60
}
pikachu.weigth // š¤ Did you mean 'weight'?
Code completion already makes it hard to make such a mistake writing regular JavaScript because of the benefits we get from TypeScript under the hood, but when we do thereās nothing to warn us.
The most common kinds of errors that programmers write can be described as type errors: a certain kind of value was used where a different kind of value was expected. This could be due to simple typos, a failure to understand the API surface of a library, incorrect assumptions about runtime behavior, or other errors. ā TypeScript Handbook
TypeScript only gives us information before we run the code. Thatās also known as static type checking.
Static type checking means your code is evaluated before it runs to ensure it works as expected.
Thatās also a limitation of TypeScript to keep in mind.
Runtime and Compile Time
TypeScript is JavaScriptās runtime with a compile time type checker. ā TypeScript Handbook
- Runtime is when JavaScript code gets executed
- Compile time is when TypeScript code gets compiled to JavaScript code
TypeScript only checks your code at compile time.
This means you canāt rely on TypeScript for checks in your code such as user input when you ship your code.
const pokemon = []
function addPokemon(pokemonName: string) {
pokemon.push(pokemonName)
}
// ['Pikachu'] ā
addPokemon('Pikachu')
// Type 'number' not assignable to type 'string'. š«
addPokemon(1)
Despite the TypeScript error, we can run the TypeScript code because type errors arenāt syntax errors.
const pokemon = []
function addPokemon(pokemonName) {
pokemon.push(pokemonName)
}
// ['Pikachu'] ā
addPokemon('Pikachu')
// oops! š©
addPokemon(1)
This is the compiled (transpiled š) JavaScript code. TypeScript didnāt betray us. We neglected to put checks and error validation in the code.
const pokemon = []
function addPokemon(pokemonName: string) {
if (!pokemonName || typeof pokemonName !== 'string') {
throw new Error('š© You have to specify a Pokemon name.')
}
pokemon.push(pokemonName)
}
// Type 'number' not assignable to type 'string'. š«
addPokemon(1)
TypeScript doesnāt change how JavaScript works.
Gradual Adoption
So far weāve seen we can reap the benefits of TypeScript without using TypeScript directly.
If youāre on the fence about TypeScript but prefer JSDoc you can take advantage of TypeScript being built-in. You can use JSDoc for types and have self-documenting code.
You can read more about JSDoc support for VS Code and look at the examples.
That being said we can adjust how strict type checking is when starting to use TypeScript so we donāt get overwhelmed making adding types a gradual adoption.
If you decide on using TypeScript you donāt have to rename everything at once but instead do it on a per-file basis.
TypeScript Introduction Summary
Letās get a clear picture of TypeScript šø:
- TypeScript is a static type checker (TypeScript checks your code before you run it)
- TypeScript is a superset of JavaScript (this means that any JavaScript program is also a valid TypeScript program)
- TypeScript preserves the runtime behavior of JavaScript (TypeScript doesnāt change how JavaScript works)
- TypesScript compiles to JavaScript
- Types are gone once it compiles to JavaScript (the browser and Node donāt understand TypeScript)
- Using TypeScript is a gradual adoption
JavaScript Types
āYou donāt need TypeScript, we have TypeScript at homeā.
True scholars š§ among you with keen intellect might observe that JavaScript already has primitive types.
A primitive type is data that is not an object and has no methods.
JavaScript has 7 primitive types:
- string (sequence of characters)
- number (floating point is the only number type)
- bigint (for huge numbers)
- boolean (logical data type with two values)
- undefined (assigned to variables that have just been declared)
- symbol (unique values)
- null (points to a nonexistent object or address)
Alongside those primitive types there are primitive wrapper objects:
- String (for the string primitive)
- Number (for the number primitive)
- BigInt (for the bigint primitive)
- Boolean (for the boolean primitive)
- Symbol (for the symbol primitive)
Letās clear up the difference between primitive types and primitive wrapper objects so you donāt get confused if you should use the lowercase string
or capitalized String
as a type.
// 'Pikachu'
const stringPrimitive = 'Pikachu'
// String { 'Pikachu' }
const stringObject = new String('Pikachu')
JavaScript converts primitive types to primitive wrapper objects behind the scenes so we can use their methods.
This is because methods like toUpperCase
extend the String
object.
The reason we donāt use primitive wrapper objects is because itās more work to get the value out of the object and we could get unexpected results if we pass an object to something expecting a primitive value.
That being said donāt confuse the new String
constructor with the String
function that does type conversion.
const number = '42'
const string = String(number) // '42'
As weāve learned TypeScript doesnāt save us at runtime, so we have to put checks in place.
function isString(value: string): boolean {
return typeof value === 'string' ? true : false
}
isString('Pikachu') // true ā
isString(1) // false š«
So why are we writing more code? Letās look at an example of a addPokemon
function.
const pokemon = []
function addPokemon(name, timeAdded) {
pokemon.push({ name, timeAdded })
}
addPokemon('Pikachu', new Date())
Writing JavaScript we have to consider a lot of things:
- Is the function callable?
- Does the function return anything?
- What are the arguments of the function?
- What date format does the argument accept?
Even if we look at the implementation of addPokemon
we donāt know what date format we should pass to timeAdded
, so we turn to documentation.
TypeScript prevents us from making those mistakes in the first place by knowing we are accessing the right properties and passing the right arguments alongside code completion.
const pokemon = []
function addPokemon(name: string, timeAdded: Date) {
pokemon.push({ name, timeAdded })
}
In the example the argument name
is of type string
and argument timeAdded
is of type Date
which is just a built-in type.
This is extremely useful when dealing with some API because the documentation lives inside your editor.
// Type 'string' is not assignable to
// parameter of type 'Date'. š«
addPokemon('Pikachu', Date())
The Date
function returns a string
but we have to pass the new Date
constructor that returns an object. The type doesnāt match so TypeScript complains that you canāt assign string
to Date
.
// [{ name: 'Pikachu', added: Date... }] ā
addPokemon('Pikachu', new Date())
Letās start learning about TypeScript and using it in practice.
TypeScript Playground
To get started open the TypeScript Playground.
The playground is a great way of seeing the compiled JavaScript code and generated TypeScript types without having to set up anything while using the same Monaco editor that powers VS Code, so you should feel at home.
The right side of the editor has some useful tabs:
- .JS shows the compiled JavaScript output (the default target is ES2017 or EcmaScript which is the name of the JavaScript specification adjustable from the TS Config tab)
- .D.TS has the generated TypeScript types
- Errors is like the error logs in your console
- Logs show the output of your code
You can press Ctrl + Enter to run the code in the playground. Another tip I have is to include console.clear()
at the top so your logs stay readable.
If you canāt see the right sidebar press the arrow icon at the top right.
Type Inference
TypeScript can infer types to provide type information.
let pokemon = 'Pikachu'
pokemon = 'Charizard'
// 'CHARIZARD' ā
pokemon.toUpperCase()
// Type 'number' is not assignable to type 'string'. š«
pokemon = 1
// This expression is not callable. š«
pokemon()
How great is this instant feedback in your editor?
TypeScript can also infer the return type of a function. If it doesnāt return anything itās void
.
function returnPokemon() {
// return string
return 'Pikachu'
}
function logPokemon() {
// we don't return anything
console.log('Pikachu')
}
Itās encouraged by the TypeScript documentation to let TypeScript infer the type when possible and later Iām going to show an example of that but you can always be explicit if you want.
function returnPokemon(): string {
// return string
return 'Pikachu'
}
function logPokemon(): void {
// we don't return anything
console.log('Pikachu')
}
In the next example weāre looking at the return type of a fetch API request.
const API = 'https://pokeapi.co/api/v2/pokemon/'
async function getPokemon(name: string) {
const response = await fetch(`${API}${name}`)
const pokemon = await response.json()
return pokemon
}
// shows Pikachu data from the Pokemon API
getPokemon('pikachu').then(console.log)
If you hover over getPokemon
you can see TypeScript infered the return type as Promise<any>
.
any
is an escape hatch when we donāt know what the type is.
const API = 'https://pokeapi.co/api/v2/pokemon/'
async function getPokemon(
name: string
): Promise<{ id: number, name: string }> {
const response = await fetch(`${API}${name}`)
const pokemon = await response.json()
return pokemon
}
// shows Pikachu data from the Pokemon API
getPokemon('pikachu').then(console.log)
Here weāre more explicit about the return type with using the object type { id: number, name: string }
.
I havenāt mentioned that we can do that because Promise<Type>
is a generic but more on that later.
const API = 'https://pokeapi.co/api/v2/pokemon/'
async function getPokemon(
name: string
): Promise<{ id: number, name: string }> {
const response = await fetch(`${API}${name}`)
const pokemon = await response.json()
return pokemon
}
async function logPokemon(name: string) {
const pokemon = await getPokemon(name)
console.log({ id: pokemon.id, name: pokemon.name })
}
// { 'id': 25, 'name': 'pikachu' }
logPokemon('pikachu')
Thanks to this slight change we get code completion for the pokemon
object since TypeScript knows the type. Itās magic. šŖ
Weāre going to go in-depth later and explore how to make this more organized by using other TypeScript features such as type alias and interface.
Type Annotations
Type annotations are an explicit way of specifying a type.
const pokemon: string = 'Pikachu'
Primitive Types
TypeScript has the same basic primitive types.
const pokemon: string = 'Pikachu'
const hp: number = 35
const caught: boolean = true
TypeScript Types
TypeScript extends the list of types:
- any
- unknown
- void
- never
any
Type any
is a special type:
- You can use type
any
as an escape hatch when you donāt want something to cause type checking errors - Type
any
represents all possible values - You get no type checking, so avoid using it
const apiResponse: any = {
data: []
}
// we don't get any warning š±
apiResponse.doesntExist
Using type any
is useful in situations where:
- Youāre working with a library that lacks types
- You have a complex API response you donāt want to type
- The API of the code youāre writing could change
If you want to focus on writing code I suggest instead of using any
everywhere to include // @ts-nocheck
at the top of your file. After youāre done writing code turn type checking back on. Donāt let your editor bully you.
Be conservative when using any
because it defeats the purpose of using TypeScript.
unknown
Type unknown
is the type-safe version of any
:
- You can assign any value to type
unknown
but you canāt do whatever you want - You must use checks to type narrow a value before you can use a it
- You canāt access any object properties unless you type narrow first and then use type assertion
- Type
unknown
can only be assigned to typeunknown
and typeany
const apiResponse: unknown = {
data: []
}
// assignable to `any` ā
const anyType: any = apiResponse
// assignable to `unknown` ā
const unknownType: unknown = apiResponse
// 'unknown' not assignable to type '{ data: []; }'. š«
const otherType: { data: [] } = apiResponse
// we have to use checks to narrow down the type
if (apiResponse && typeof apiResponse === 'object') {
// Property 'data' does not exist on type 'object'. š«
apiResponse.data
}
We narrowed down what the type of apiResponse
is, yet we canāt access the apiResponse.data
property since itās unknown
.
To solve the problem we have to use type assertion that lets TypeScript know about the type.
if (apiResponse && typeof apiResponse === 'object') {
const response = apiResponse as { data: [] }
response.data // no warning ā
}
Weāre going to learn about type assertion later.
unknown
is safer to use than any
when we donāt know the function argument, so instead of being able to do anything inside prettyPrint
we have to narrow the type of the input
argument first to be able to use it.
function prettyPrint(input: unknown): string {
if (Array.isArray(input)) {
// we can run each value through prettyPrint again
return input.map(prettyPrint).join(', ')
}
if (typeof input === 'string') {
return input
}
if (typeof input === 'number') {
return String(input)
}
return '...'
}
const values = ['Bulbasaur', 'Charmander', 'Squirtle', 1, {}]
const prettyValues = prettyPrint(values)
// 'Bulbasaur, Charmander, Squirtle, 1, ...'
console.log(prettyValues)
We can use unknown
to describe a function that returns an unknown value. Because obj
is unknown
we have to narrow
the type first before we do something reckless making it safe.
const json = '{ "id": 1, "name": "Pikachu" }'
function safeParse(value: string): unknown {
return JSON.parse(value)
}
const obj = safeParse(json) // safe ā
If we didnāt use unknown
the infered return type for safeParse
would be any
meaning you could do whatever you want with obj
.
You should avoid using any
or unknown
if possible.
void
Type void
is the absence of having any type.
Thereās no point assigning void
to a variable since only type undefined
is assignable to type void
.
let pokemon: void
// only `undefined` is assignable to `void` ā
pokemon = undefined
// Type 'string' is not assignable to type 'void'. š«
pokemon = 'Pikachu'
You mostly see type void
used on functions that donāt return anything.
function logPokemon(pokemon: string): void {
console.log(pokemon)
}
logPokemon('Pikachu') // 'Pikachu'
Letās learn how type void
is useful when used in a forEach
implementation.
function forEach(
arr: any[],
callback: (arg: any, index?: number) => void
): void {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i)
}
}
forEach(
['Bulbasaur', 'Charmander', 'Squirtle'],
(pokemon) => console.log(pokemon)
)
Because we use type void
as the return type for forEach
weāre saying the return value isnāt going to be used, so it can be called with a callback that returns any value.
Using the return type void
explicity can save us from returning a value on accident during refactor.
function logPokemon(pokemonList: string[]): void {
pokemonList.forEach(pokemon => {
// ...
return pokemon
})
}
function logPokemonRefactor(pokemonList: string[]): void {
for (const pokemon of pokemonList) {
// ...
// Type 'string' is not assignable to type 'void'. š«
return pokemon
}
}
never
Type never
represents values that never occur:
- Type
never
canāt have a value - You use type
never
when thereās no reachable end point like a while loop or error exception - Variables get the type
never
when narrowed by type guards to remove possibilities (a great example is preventing impossible states when a prop is passed to a component where you can say if one type of prop gets passed another canāt)
function infiniteLoop(): never {
while (true) {
// ...
}
}
function error(message: string): never {
throw new Error(message)
}
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('ā Timed out.')), ms)
})
}
Type unknown
can be used together with type narrowing to ensure we have a check for each Pokemon type.
function getPokemonByType(
pokemonType: 'fire' | 'water' | 'electric'
) {
// type is 'fire' | 'water' | 'electric'
if (pokemonType === 'fire') {
return 'š„ Fire Pokemon'
}
// we narrow it down to 'water' | 'electric'
if (pokemonType === 'water') {
return 'š Water Pokemon'
}
// only 'electric' is left
pokemonType
// remainingPokemonTypes can't have any value
// because pokemonType is 'electric' š«
const remainingPokemonTypes: never = pokemonType
return remainingPokemonTypes
}
getPokemonByType('electric')
Because of this check we know that getPokemonByType
is missing a check for the electric
type.
We havenāt yet learned about some of the types in the examples. Weāre not glossing over them. Weāre going to cover each later.
Array Types
Thereās two equivalent ways to specify an array type in TypeScript.
To specify an array type you can use the generics Array<Type>
syntax or the Type[]
syntax.
const pokemon1: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']
const pokemon2: string[] = ['Bulbasaur', 'Charmander', 'Squirtle']
I prefer the Type[]
syntax because itās less to type.
Function Types
You can specify the input and output type of functions.
const pokemon: string[] = []
function addPokemon(name: string): string[] {
pokemon.push(name)
return pokemon
}
addPokemon('Pikachu')
We explicitly typed pokemon
as string[]
. If TypeScript canāt infer the type it would use any
by default, or in this case the return type of addPokemon
would be any[]
.
Anonymous functions besides using contextual typing arenāt any different and using type annotations is the same.
const pokemon: string[] = []
const addPokemon = (name: string): string[] => {
pokemon.push(name)
return pokemon
}
addPokemon('Pikachu')
TypeScript uses contextual typing to figure out the type of the argument based on how itās used inside an anonymous function.
const pokemonList = ['Bulbasaur', 'Charmander', 'Squirtle']
// 'Bulbasaur', 'Charmander', 'Squirtle'
pokemonList.forEach(pokemon => console.log(pokemon))
TypeScript infered pokemonList
is of type string[]
. Because of this inside forEach
it knows the individual pokemon
type should be of type string
.
// being explicit ā
const pokemonList: string[] = ['Bulbasaur', 'Charmander', 'Squirtle']
// this is redundant š«
pokemonList.forEach((pokemon: string) => console.log(pokemon))
Knowing about contextual typing you understand what things you donāt have to type.
Your goal isnāt to please TypeScript but use it to give you confidence your code works as expected.
Function arguments can be made optional by using the ?
operator.
function logPokemon(name: string, hp?: number) {
console.log({ name, hp })
}
// { name: 'Pikachu', hp: undefined } ā
logPokemon('Pikachu')
// { name: 'Pikachu', hp: 35 } ā
logPokemon('Pikachu', 35)
Function Overloads
Function overloading is the ability to create multiple functions of the same name with different implementations. Which implementation gets used depends on the arguments you pass in.
In JavaScript there is no function overloading because we can pass any number of parameters of any type we then perform checks on inside the function.
function logPokemon(arg1, arg2) {
if (typeof arg1 === 'string' && typeof arg2 === 'number') {
console.log(`${arg1} has ${arg2} HP.`)
}
if (typeof arg1 === 'object') {
const { name, hp } = arg1
console.log(`${name} has ${hp} HP.`)
}
}
// 'Pikachu has 35 HP.' ā
logPokemon('Pikachu', 35)
// 'Pikachu has 35 HP.' ā
logPokemon({ name: 'Pikachu', hp: 35 })
TypeScript has overload signatures that let you call a function in different ways.
interface Pokemon {
name: string
hp: number
}
function logPokemon(name: string, hp: number): void
function logPokemon(pokemonObject: Pokemon): void
function logPokemon(arg1: unknown, arg2?: unknown): void {
// matches the first overload signature
if (typeof arg1 === 'string' && typeof arg2 === 'number') {
// arg1 is `name` and arg2 is `hp`
console.log(`${arg1} has ${arg2} HP.`)
}
// matches the second overload signature
if (typeof arg1 === 'object') {
// since it's an object we can assert the type to be Pokemon
const { name, hp } = arg1 as Pokemon
// log the destructured values
console.log(`${name} has ${hp} HP.`)
}
}
// 'Pikachu has 35 HP.' ā
logPokemon('Pikachu', 35)
// 'Pikachu has 35 HP.' ā
logPokemon({ name: 'Pikachu', hp: 35 })
Letās break it down into steps:
- We wrote two overload signatures for
logPokemon
- The first overload signature accepts
name
andhp
arguments - The second overload signature accepts a
pokemonObject
argument - After that we wrote a function implementation with a compatible signature where the second
arg2
argument is optional since the minimal amount of arguments is one - If
arg1
isstring
andarg2
isnumber
we know it matches the first signature - If
arg1
is anobject
we know it matches the second signature, so we can use type assertion and destructure thename
andhp
values from it
Object Types
The object type is like a regular object in JavaScript.
If you hover over pokemonInferedType
TypeScript already knows itās shape.
// infered type
const pokemonInferedType = {
name: 'Pikachu'
}
// explicit type
const pokemonExplicitType: { name: string } = {
name: 'Pikachu'
}
In the next examples we can see how TypeScript treats missing, optional, and extra object properties.
// Property 'id' is missing in type '{ name: string; }' but
// required in type '{ id: number; name: string; }'. š«
const pokemonMissingProperty: { id: number, name: string } = {
name: 'Pikachu'
}
// `id` is optional ā
const pokemonOptionalArgument: { id?: number, name: string } = {
name: 'Pikachu'
}
// Object literal may only specify known properties. š«
const pokemonExtraProperty: { id?: number, name: string } = {
id: 1,
name: 'Pikachu',
pokemonType: 'electric'
}
Weāre going to learn ways to abstract types to make them more readable and reusable next.
Type Aliases
So far weāve been using types directly using type annotations. This is hard to read and not reusable.
A type alias is as the name suggests ā just an alias for a type.
Just how we assign names to different types š¤ of people.
You should already be familiar with the object type syntax.
type Pokemon = { id: number, name: string, pokemonType: string }
const pokemon: Pokemon[] = [{
id: 1,
name: 'Pikachu',
pokemonType: 'electric'
}]
// { 'id': 1, 'name': 'Pikachu', 'pokemonType': 'electric' }
pokemon.forEach(pokemon => console.log(pokemon))
Instead of writing Pokemon[]
where []
indicates to TypeScript itās an array of Pokemon we can say { id: number, name: string, type: string }[]
which is equivalent.
type Pokemon = { id: number, name: string, pokemonType: string }[]
const pokemon: Pokemon = [{
id: 1,
name: 'Pikachu',
pokemonType: 'electric'
}]
// { 'id': 1, 'name': 'Pikachu', 'pokemonType': 'electric' }
pokemon.forEach(pokemon => console.log(pokemon))
I prefer the Pokemon[]
syntax because of reusability. We can use the Pokemon
type on single Pokemon where it makes sense and Pokemon[]
on a collection of Pokemon.
This saves us from creating another type and using weird grammar like Pokemons
to indicate thereās many despite Pokemon already being plural.
The next example shows how we can reuse the Pokemon
type by using it as the argument and return type of the logPokemon
function.
type Pokemon = string[] | string
function logPokemon(pokemon: Pokemon): Pokemon {
if (Array.isArray(pokemon)) {
return pokemon.map(pokemon => pokemon.toUpperCase())
}
if (typeof pokemon === 'string') {
return pokemon.toUpperCase()
}
return 'Please enter a Pokemon.'
}
// ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE']
console.log(logPokemon(['Bulbasaur', 'Charmander', 'Squirtle']))
// 'PIKACHU'
console.log(logPokemon('Pikachu'))
// 'Please enter a Pokemon.'
console.log(logPokemon())
Functions are just special objects in JavaScript which is a roundabout way of saying we can type them as any other object.
The next examples shows ways to type different function expressions using a type alias.
type LogPokemon = (pokemon: string) => void
// named function expression
const logPokemon1: LogPokemon = function logPokemon(pokemon) {
console.log(pokemon)
}
// anonymous function expression
const logPokemon2: LogPokemon = function(pokemon) {
console.log(pokemon)
}
// anonymous arrow function expression
const logPokemon3: LogPokemon = (pokemon) => console.log(pokemon)
The next example shows how we can use a construct signature inside a type alias to type a constructor function.
type Pokemon = {
name: string
pokemonType: string
}
type PokemonConstructor = {
new(name: string, pokemonType: string): Pokemon
}
class PokemonFactory implements Pokemon {
name: string
pokemonType: string
constructor(name: string, pokemonType: string) {
this.name = name
this.pokemonType = pokemonType
}
}
function addPokemon(
pokemonConstructor: PokemonConstructor,
name: string,
pokemonType: string
): Pokemon {
return new pokemonConstructor(name, pokemonType);
}
const pokemon: Pokemon = addPokemon(
PokemonFactory,
'Pikachu',
'electric'
)
// PokemonFactory: { "name": "Pikachu", "pokemonType": "electric" }
console.log(pokemon)
Confusing, right? This is just showing you itās possible, so donāt think about it.
Interfaces
Interfaces are another way to name an object type.
interface Pokemon {
id?: number
name: string
pokemonType: string
ability: string
attack(): void
}
const pokemon: Pokemon[] = [{
id: 1,
name: 'Pikachu',
pokemonType: 'electric',
ability: 'ā” Thunderbolt',
attack() {
console.log(`${this.name} used ${this.ability}.`)
}
}]
// 'Pikachu used ā” Thunderbolt'
pokemon.forEach(pokemon => pokemon.attack())
Inside the interface we can say properties are optional using ?
and type function signatures like attack(): void
.
Interfaces and type aliases are almost interchangable.
You can also use a semicolon or period after each property if you want as itās purely optional.
Type Aliases or Interfaces?
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from type. - TypeScript Handbook
The short answer is ā doesnāt matter. Pick one and if it doesnāt work for you use the other one.
I use both when it makes sense:
- Interface is more appropriate for describing shapes of objects
- You can add new fields to an existing interface but not to a type alias
We havenāt learned about intersections yet but briefly it just lets us combine types.
The next example shows how using intersections we can extend a type alias by combining the type Pokemon
and { pokemonType: 'electric' }
into a new type Electric
.
type Pokemon = {
id: number
name: string
}
type Electric = Pokemon & { pokemonType: 'electric' }
// has to satisfy the same checks ā
const pikachu: Electric = {
id: 1,
name: 'Pikachu',
pokemonType: 'electric'
}
In the case of interfaces we use the extends keyword to extend them.
interface Pokemon {
id: number
name: string
}
interface Electric extends Pokemon {
pokemonType: 'electric'
}
// has to satisfy the same checks ā
const pikachu: Electric = {
id: 1,
name: 'Pikachu',
pokemonType: 'electric'
}
When using an interface you should always keep in mind that you can use an existing interface which could lead to some unexpected results.
interface Window {
color: string
style: 'double-hung' | 'casement' | 'awning' | 'slider'
}
// Type '{ color: string; style: "double-hung"; }' is missing
// the following properties from type 'Window': applicationCache,
// clientInformation, closed, customElements, and 207 more. š«
const item: Window = {
color: 'white',
style: 'double-hung'
}
The type Window
is already declared in lib.dom.d.ts
types for the global window
object. š©
Letās briefly touch upon naming conventions. For the most part I just name the type the capitalized version of what Iām trying to type. For example pokemon
would be Pokemon
.
Sometimes that wonāt work like in the case of React where component names are capitalized so you can use a suffix such as PokemonProps
.
For whatever reason using the prefix IPokemon
for an interface is controversial inside TypeScript circles but you probably shouldnāt care as long as your naming convention is consistent.
Union Types
A union type is a type made from at least two types and represents any values of those types.
function logPokemon(pokemon: string[] | string) {
console.log(pokemon)
}
// ['Bulbasaur', 'Charmander', 'Squirtle'] ā
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
// Pikachu ā
logPokemon('Pikachu')
The pokemon
argument can only be an array of Pokemon of type string[]
or a single Pokemon of type string
.
You can only do things with the union type that every member supports meaning that you canāt just use a string
method without checks because you said to TypeScript the type could either be string[]
or string
.
function logPokemon(pokemon: string[] | string) {
// Property 'toUpperCase' does not exist on type 'string[]'. š«
console.log(pokemon.toUpperCase())
}
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
logPokemon('Pikachu')
Instead we have to put checks in place to narrow down the type so TypeScript knows the exact type.
function logPokemon(pokemon: string[] | string) {
if (Array.isArray(pokemon)) {
// `pokemon` can only be an array ā
console.log(pokemon.map(pokemon => pokemon.toUpperCase()))
}
if (typeof pokemon === 'string') {
// `pokemon` can only be string ā
console.log(pokemon.toUpperCase())
}
}
// ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE']
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
// PIKACHU
logPokemon('Pikachu')
Unfortunately the second if...else
statement is required because TypeScript canāt narrow down the pokemon
type to string
.
In situations where the union members like string[]
and string
overlap and share the same methods such as slice
you donāt have to narrow the type.
function logPokemon(pokemon: string[] | string) {
// works for both types ā
console.log(pokemon.slice(0, 1))
}
// ['Bulbasaur']
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
// 'P'
logPokemon('Pikachu')
Discriminated Unions
A discriminated union is the result of narrowing the members of the union that have the same literal type.
For example we can use a type guard to narrow the Pokemon type 'fire' | 'water'
to 'fire'
or 'water'
.
The next example shows what you might expect would work but doesnāt because TypeScript canāt determine if the property exists.
Letās switch š¤ it up for fun.
interface Pokemon {
flamethrower?: () => void
whirlpool?: () => void
pokemonType: 'fire' | 'water'
}
function pokemonAttack(pokemon: Pokemon) {
switch (pokemon.pokemonType) {
case 'fire':
// Cannot invoke an object which is possibly 'undefined'. š«
pokemon.flamethrower()
break
case 'water':
// Cannot invoke an object which is possibly 'undefined'. š«
pokemon.whirlpool()
break
}
}
// š„ 'Flamethrower'
pokemonAttack({
pokemonType: 'fire',
flamethrower: () => console.log('š„ Flamethrower')
})
// š 'Whirlpool'
pokemonAttack({
pokemonType: 'water',
whirlpool: () => console.log('š Whirlpool')
})
The type-checker canāt determine if flamethrower
or whirpool
is present based on the pokemonType
property because they could not exist as theyāre both optional.
To solve this problem we have to be more explicit and separate the arguments so TypeScript can be sure of the type.
interface Fire {
flamethrower: () => void
pokemonType: 'fire'
}
interface Water {
whirlpool: () => void
pokemonType: 'water'
}
type Pokemon = Fire | Water
function pokemonAttack(pokemon: Pokemon) {
switch (pokemon.pokemonType) {
case 'fire':
pokemon.flamethrower() // ā
break
case 'water':
pokemon.whirlpool() // ā
break
}
}
// š„ 'Flamethrower'
pokemonAttack({
pokemonType: 'fire',
flamethrower: () => console.log('š„ Flamethrower')
})
// š 'Whirlpool'
pokemonAttack({
pokemonType: 'water',
whirlpool: () => console.log('š Whirlpool')
})
By the use of a type guard style check ==
, ===
, !=
, !==
or switch
on the discriminant property pokemonType
TypeScript can do type narrowing based on the literal type.
This also helps us catch mistakes if something passed through the switch
statement that shouldnāt have.
Intersection Types
Intersection types let us combine types using the &
operator.
The next example also shows how we can use interfaces and type aliases together.
interface Pokemon {
name: string
hp: number
pokemonType: [string, string?]
}
interface Ability {
blaze(): void
}
interface Moves {
firePunch(): void
}
type Fire = Ability & Moves
type FirePokemon = Pokemon & Fire
const charizard: FirePokemon = {
name: 'Charizard',
hp: 100,
pokemonType: ['fire', 'flying'],
blaze() {
console.log(`${this.name} used š„ Blaze.`)
},
firePunch() {
console.log(`${this.name} used š„ Fire Punch.`)
}
}
charizard.blaze() // 'Charizard used š„ Blaze.'
charizard.firePunch() // 'Charizard used š„ Fire Punch.'
I threw in a sneaky tuple in pokemonType
because a Pokemon can have dual-types. Weāre going to learn more about tuple later.
If we wanted to use an interface we could and it works just the same.
interface Fire extends Ability, Moves {}
interface FirePokemon extends Pokemon, Fire {}
Itās easier to compose types using type aliases.
Type Assertion
Type assertion is like type casting in TypeScript where it can be used to specify or override another type.
In this example TypeScript only knows that formEl
returns some kind of HTMLElement
or null
from document.getElementById
meaning we canāt use some of the built-in form methods.
// formEl is `HTMLElement | null` because it might not exist
const formEl = document.getElementById('form')
These types correspond to the browser API. Element is the base class other elements inherit from. HTMLElement is the base interface for HTML elements. Thatās how we get to the HTMLFormElement that represents a <form>
element in the DOM (Document Object Model).
const formEl = document.getElementById('form')
// Property 'reset' does not exist on type 'HTMLElement'. š«
formEl?.reset()
If youāre not familiar with the optional chaining operator it just says to TypeScript the value canāt be null
.
// short-circuit evaluation
formEl && formEl.reset()
// optional chaining equivalent
formEl?.reset()
Itās a relatively recent addition to JavaScript but itās been a part of TypeScript for a while, so you might not be familiar with it. If the value doesnāt exist itās just going to return undefined
.
If we look at the API for HTMLElement the TypeScript error makes complete sense since that method doesnāt exist.
We can be more specific about the type of element with type assertion using the as
keyword.
const formEl = document.getElementById('form') as HTMLFormElement
formEl?.reset() // works ā
The š§ pro-tip for how to figure out what element you want is fumbling around until your code completion gives you an option that looks like what you want.
Thereās also the alternative angle bracket <>
syntax for type assertion.
const formEl = <HTMLFormElement>document.getElementById('form')
formEl?.reset() // works ā
Donāt confuse this syntax with TypeScript generics.
The angle bracket syntax is avoided because it gets mistaken for React components because of JSX.
We can observe this error in the TypeScript Playground because in the TS Config the JSX for React option is on by default.
// JSX element 'HTMLFormElement' has no corresponding closing tag. š«
const formEl = <HTMLFormElement>document.getElementById('form')
Event listeners are a big part of JavaScript and something we use often even in JavaScript frameworks.
In the next example we have an input field with an event listener that takes a Pokemon name.
<input type="text" id="pokemon" />
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement
function handleInput(event) {
// ...
}
pokemonInputEl.addEventListener('input', (event) => handleInput(event))
When you hover over event
in addEventListener
notice the infered type Event
.
This makes sense because Event is the base interface for events. Derived from Event thereās UIEvent that has other interfaces like MouseEvent, TouchEvent and KeyboardEvent among others.
This is a teachable moment that lets us know we can leverage TypeScript to help us figure out built-in types instead of having to look it up.
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement
function handleInput(event: Event) {
// Object is possibly 'null'. š«
// Property 'value' does not exist on type 'EventTarget'. š«
event.target.value
}
pokemonInputEl.addEventListener('input', (event) => handleInput(event))
The problem we face has to do again with the type not being specific enough. We want the value
from event.target
but we can see the type is EventTarget
so TypeScript doesnāt let us access that property.
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement
function handleInput(event: Event) {
const targetEl = event.target! as HTMLInputElement
targetEl.value
}
pokemonInputEl.addEventListener('input', (event) => handleInput(event))
TypeScript knows the type is HTMLInputElement
, so we can use itās methods and properties.
You donāt have to keep this knowledge in your head. Let TypeScript and your editor help you figure out what type to use.
// event is `MouseEvent` š
window.addEventListener('mouseover', (event) => console.log('Mouse event'))
// event is `TouchEvent` š
window.addEventListener('touchmove', (event) => console.log('Touch event'))
// event is `KeyboardEvent` š¹
window.addEventListener('keyup', (event) => console.log('Keyboard event'))
In the previous example we used !
that asserts the type canāt be null
but keep in mind that when using any type assertion youāre saying the element canāt be null
.
// HTMLElement | null
const pokemonInputEl = document.getElementById('pokemon')
// HTMLInputElement
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement
That being said donāt lie to the TypeScript compiler and only use type assertion when you have to.
!
Type Assertion Using Using the !
syntax after any expression is a type assertion that the value isnāt going to be null
or undefined
.
const formEl = document.getElementById('form')! as HTMLFormElement
formEl.reset() // works ā
Type Assertion Conversion
Using any
and type assertion is great when youāre just writing code but avoid using them unless you have to.
TypeScript tries itās best to not let you do something stupid.
// 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. š«
const pokemon = 'Pikachu' as number
That doesnāt mean you canāt do whatever you want.
const pokemon = 'Pikachu' as unknown as number
// looks great š„
pokemon.toFixed()
TypeScript only allows type assertions that convert to a more specific or less specific version of a type.
Literal Types
Literal types are exact values of strings and numbers.
Both
var
andlet
allow for changing what is held inside the variable, andconst
does not. This is reflected in how TypeScript creates types for literals. ā TypeScript Handbook
TypeScript has string
, number
, and boolean
literals. The boolean
type itself is just an alias for the true | false
union.
// type is string
let pokemonGeneralType = 'Pikachu'
// type is 'Pikachu'
const pokemonLiteralType = 'Pikachu'
This concept is more useful if we combine type literals into unions.
function movePokemon(
direction: 'up' | 'right' | 'down' | 'left'
) {
// ...
}
movePokemon('up') // acceptable value ā
movePokemon('rigth') // oops! typo. š«
Literal Inference
Literal inference is when TypeScript thinks properties on an object might change, so instead of infering a literal type it infers a primitive type.
Types are used to determine reading and writing behavior.
function addPokemon(
name: string,
pokemonType: 'š„ fire' | 'š water' | 'ā” electric'
) {
console.log({ name, pokemonType })
}
const pokemon = {
name: 'Pikachu',
pokemonType: 'ā” electric'
}
// Argument of type 'string' is not assignable to
// parameter of type '"š„ fire" | "š water" | "ā” electric"'. š«
// `pokemonType` is 'string' instead of 'ā” electric' š«
addPokemon(pokemon.name, pokemon.pokemonType)
We can use type assertion on pokemonType
.
const pokemon = {
name: 'Pikachu',
pokemonType: 'ā” electric' as 'ā” electric'
}
The easier method is using the as const
suffix that acts like the type equivalent of const
.
const pokemon = {
name: 'Pikachu',
pokemonType: 'ā” electric'
} as const
TypeScript adds a readonly
type on the pokemon
properties that signals they wonāt change, so TypeScript knows itās a literal type instead of a general type like string
or number
.
const pokemon: {
readonly name: "Pikachu"
readonly pokemonType: "electric"
}
This signals to TypeScript the values of pokemon
wonāt change.
Object Index Signatures
When we donāt know the properties of an object ahead of time but know the shape of the values we can use index signatures.
JavaScript has two ways to access a property on an object:
- The dot operator:
object.property
- Square brackets:
object['property']
The second syntax is called index accessors.
This is reflected in the types system where you can add an index signature to unknown properties.
interface PokemonAPIResponse {
[index: string]: unknown
}
This shows the Pokemon API response.
Weāre using an index signature syntax [key: type]: type
to indicate that the keys of the object are going to be a string
with an unknown
property.
This is useful when we donāt have control over an API or time to type out a complex interface.
In the example we might only care about some properties and not the rest.
interface PokemonAPIResponse {
// let any other property through that
// matches the index signature
[index: string]: unknown
base_experience: number
id: number
name: string
abilities: Array<{ ability: { name: string, url: string }}>
moves: Array<{ move: { name: string, url: string }}>
}
const API = 'https://pokeapi.co/api/v2/pokemon/'
async function getPokemon(
name: string
): Promise<PokemonAPIResponse> {
const response = await fetch(`${API}${name}`)
const pokemon = await response.json()
return pokemon
}
async function logPokemon(name) {
const pokemon = await getPokemon(name)
// Object is of type 'unknown'. š«
console.log(pokemon.order.toString())
// we need checks in place for properties we don't know about
console.log((pokemon.order as number).toString())
// we get great code completion for properties we know about
pokemon.abilities.forEach(
({ ability }) => console.log(ability.name)
)
}
logPokemon('pikachu') // '35', 'static', 'lightning-rod'
Hereās another example where we have Pokemon ratings but donāt know all the Pokemon ahead of time.
type Rating = 1 | 2 | 3 | 4 | 5
interface PokemonRatings {
[pokemon: string]: Rating
bulbasaur: Rating
charmander: Rating
squirtle: Rating
}
This is a great opportunity to explain something that might confuse you when dealing with dynamic code where youāre accesing object properties such as object[key]
where key
is dynamic.
interface Stats {
id: number
hp: number
attack: number
defense: number
}
interface Pokemon {
bulbasaur: Stats
charmander: Stats
squirtle: Stats
}
const pokemon: Pokemon = {
bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
squirtle: { id: 3, hp: 44, attack: 48, defense: 65 }
}
const bulbasaur: string = 'bulbasaur'
// No index signature with a parameter of type 'string'
// was found on type 'Pokemon'. š«
pokemon[bulbasaur]
TypeScript thinks weāre trying to access the object property by using a string index when objects are number indexed by default.
In reality we want to access the pokemon
object using the type literal bulbasaur
instead of string
.
We can see this is true if we change the index signature to string
meaning we can pass anything that matches that index signature.
interface Pokemon {
[pokemon: string]: Stats
bulbasaur: Stats
charmander: Stats
squirtle: Stats
}
// careful if this is what you want ā ļø
// general type 'string' ā
pokemon[bulbasaur]
What you should do is make sure youāre passing a type literal instead or use the as const
assertion. Iām only explicit here so itās obvious but using const already does that.
const bulbasaur: 'bulbasaur' = 'bulbasaur'
// string literal 'bulbasaur' ā
pokemon[bulbasaur]
Type Narrowing
Type narrowing is when you narrow types to more specific types, so you can limit what you can do with a certain value.
function getPokemonByType(
pokemonType: 'fire' | 'water' | 'electric'
) {
// type is 'fire' | 'water' | 'electric'
if (pokemonType === 'fire') {
return 'š„ Fire type Pokemon'
}
// we narrow it down to 'water' | 'electric'
if (pokemonType === 'water') {
return 'š Water type Pokemon'
}
// we narrow it down to 'electric'
pokemonType
// TypeScript knows only 'electric' is possible
return 'ā” Electric type Pokemon'
}
getPokemonByType('electric')
Letās appreciate for a moment how cool it is that TypeScript just knows the type weāre dealing with based on analyzing the code flow. š¤Æ
The next example shows type narrowing using the in
operator that checks if a property is in the object.
type Fire = { flamethrower: () => void }
type Water = { whirlpool: () => void }
type Electric = { thunderbolt: () => void }
function pokemonAttack(pokemon: Fire | Water | Electric) {
if ('flamethrower' in pokemon) {
pokemon.flamethrower()
}
if ('whirlpool' in pokemon) {
pokemon.whirlpool()
}
if ('thunderbolt' in pokemon) {
pokemon.thunderbolt()
}
}
const pokemon = {
name: 'Pikachu',
thunderbolt() {
console.log(`${this.name} used ā” Thunderbolt.`)
}
}
pokemonAttack(pokemon) // 'Pikachu used ā” Thunderbolt.'
If you remember from a previous example thereās a distinction between the constructor new Date()
that returns an object
and function Date()
that returns a string
.
In the same way we can use instanceof
to check if a value is an instance of another value.
function logDate(date: Date | string) {
if (date instanceof Date) {
console.log(date.toUTCString())
} else {
console.log(date.toUpperCase())
}
}
// 'THU JUL 01 2021 20:00:00 GMT+0200 (CENTRAL EUROPEAN SUMMER TIME)'
logDate(Date())
// 'Thu, 01 Jul 2021 20:00:00 GMT'
logDate(new Date())
You can read more about narrowing that covers some plain JavaScript concepts if youāre not familiar with concepts such as truthiness and equality checks.
Type Guards
A type guard is a check against the value returned by typeof
.
Because TypeScript often knows more than us about some intricacies of JavaScript it can save us from some JavaScript quirks.
function logPokemon(pokemon: string[] | null) {
if (typeof pokemon === 'object') {
// Object is possibly 'null'. š«
pokemon.forEach(pokemon => console.log(pokemon))
}
return pokemon
}
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
TypeScript lets us know the pokemon
type was narrowed down to string[] | null
instead of just string[]
.
In logPokemon
we naively check if pokemon
is an array by checking if itās an object
.
This isnāt a bad assumption since almost everything in JavaScript is an object. We donāt know one thing ā null
is also an object.
In JavaScript null
is a primitive value that returns object
.
console.log(typeof null) // 'object'
This mistake is part of the JavaScript language. You can read The history of ātypeof nullā to learn why that is so.
Type Predicates
Type predicates are a special return type thatās like a type guard for functions.
Take for example a Pokemon list where not every item is a Pokemon. We can use the isPokemon
function to check if a value is a Pokemon. In filterPokemon
the pokemonList
array is of type unknown[]
so pokemon
is unknown
.
We expect TypeScript to narrow the type through the if...else
statement to make sure the item weāre passing is a Pokemon, so we can access itās properties but pokemon
is unknown
.
interface Pokemon {
name: string
itemType: string
}
const pokemonList: Pokemon[] = [
{
name: 'Pikachu',
itemType: 'pokemon',
},
{
name: 'Berry',
itemType: 'consumable'
}
]
function isPokemon(value: any): boolean {
return value.itemType === 'pokemon' ? true : false
}
function filterPokemon(pokemonList: unknown[]) {
return pokemonList.filter(pokemon => {
if (isPokemon(pokemon)) {
// Object is of type 'unknown'. š«
console.log(pokemon.name)
}
})
}
filterPokemon(pokemonList) // 'Pikachu'
How do we narrow the type of pokemon
to be Pokemon
?
We can use type predicates to narrow the pokemon
type to Pokemon
by using the argumentName is Type
syntax that changes the argument type if the function returns true
.
function isPokemon(value: any): value is Pokemon {
return value.itemType === 'pokemon' ? true : false
}
The pokemon
type is narrowed to Pokemon
and we can access itās properties.
Generics
Generics are variables for types.
We use generics to create reusable pieces of code that can work over a variety of types rather than a single one.
The first generic we have encountered was the array type.
const pokemon: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']
The Array
type is using an interface Array<T>
where T
represents a type variable. That means we can pass our own types.
const pokemon: Array<{ name: string, pokemonType: string }> = [{ name: 'Pikachu', pokemonType: 'electric' }]
To make the code more readable we can use a type alias or interface.
interface Pokemon {
name: string
pokemonType: string
}
const pokemon: Array<Pokemon> = [{ name: 'Pikachu', pokemonType: 'electric' }]
If you remember what we learned before, instead of the Array<Pokemon>
syntax we can use the equivalent Pokemon[]
syntax.
Letās look at an example where we use logPokemon
to log Pokemon where we donāt know the type ahead of time because a user could pass anything from a string
to an array
.
function logPokemon(pokemon: any) {
return pokemon
}
const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
Hovering over the values we can see the infered type for logPokemon
is (pokemon: any): any
and log1
and log2
types are any
.
We might think of using a union type to handle the types but itās not ideal since it wouldnāt narrow the type without type guards.
function logPokemon(pokemon: string | string[]) {
return pokemon
}
const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
// Property 'toUpperCase' does not exist on type 'string[]'. š«
log1.toUpperCase()
// Property 'map' does not exist on type 'string'. š«
log2.map(pokemon => pokemon.toUpperCase)
Hovering over logPokemon
the call signature type is (pokemon: string | string[]): string | string[]
and the type for log1
and log2
is string | string[]
.
It would be easier to let the user pass in their own type using generics.
The generics syntax is <Type>
where Type
represents the type variable. You might see the single letter T
used instead but I think itās confusing. It makes the code harder to read and you might assume you have to use T
as a type name. You can name the type variable anything you want.
function logPokemon<Type>(pokemon: Type): Type {
return pokemon
}
const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
// string ā
log1.toUpperCase()
// string of arrays ā
log2.map(pokemon => pokemon.toUpperCase)
In the above example weāre saying the logPokemon
is a generic function that takes a type parameter Type
and an argument pokemon
of Type
and returns Type
.
Hovering over logPokemon
for log1
we can see the call signature matches the string we passed in logPokemon<"Pikachu">(pokemon: "Pikachu"): "Pikachu"
and log1
is the type literal Pikachu.
Hovering over logPokemon
for log2
we can see the call signature matches the array we passed in logPokemon<string[]>(pokemon: string[]): string[]
and log2
is the type string[]
.
Like weāve seen in the first example we can be explicit when using generics and pass the type directly.
const log1 = logPokemon<string>('Pikachu')
const log2 = logPokemon<string[]>(['Bulbasaur', 'Charmander', 'Squirtle'])
You can also nest generics which can get hard to read, for example logPokemon<Array<Pokemon>>
.
In the next example weāre going to look at a map
array method implementation that takes advantage of generics.
Letās think about the problem:
- We canāt know the type of
arr
since youāre able to pass any type of array - The
callback
function could have any argument and return type based on that array formatPokemon
is of typeany
because we donāt know the return type
function map(
arr: any,
callback: (arg: any) => any
): any {
return arr.map(callback)
}
const formatPokemon = map(
['Bulbasaur', 'Charmander', 'Squirtle'],
(pokemon) => pokemon.toUpperCase()
)
// ["BULBASAUR", "CHARMANDER", "SQUIRTLE"]
console.log(formatPokemon)
So far we have only seen generics with one type variable but we can use as many type variables as we want.
Hovering over formatPokemon
in the log we see itās of type string[]
and map
has the signature type of map<string, string>(arr: string[], callback: (arg: string) => string): string[]
as you would expect.
Using proper names for type variables makes the code so much easier to reason about.
function map<Input, Output>(
arr: Input[],
callback: (arg: Input) => Output
): Output[] {
return arr.map(callback)
}
const formatPokemon = map(
['Bulbasaur', 'Charmander', 'Squirtle'],
(pokemon) => pokemon.toUpperCase()
)
// ["BULBASAUR", "CHARMANDER", "SQUIRTLE"]
console.log(formatPokemon)
Generic Interface
An interface can also be generic.
Weāre creating a generic interface Dictionary
with a index signature that accepts any string
as the object key and itās property has to match the shape of Type
.
interface Dictionary<Type> {
[key: string]: Type
}
interface Pokemon {
name: string
hp: number
pokemonType: string
}
interface Consumable {
name: string
amount: number
}
const pokemon: Dictionary<Pokemon> = {
1: { name: 'Bulbasaur', hp: 45, pokemonType: 'grass' },
2: { name: 'Charmander', hp: 39, pokemonType: 'fire' },
3: { name: 'Squirtle', hp: 44, pokemonType: 'water' }
}
const consumables: Dictionary<Consumable> = {
1: { name: 'Antidote', amount: 4 },
2: { name: 'Potion', amount: 8 },
3: { name: 'Elixir', amount: 2 }
}
Generic Constraints
So far weāve seen generics that work with any kind of value. We can use a constraint to limit what a type parameter can accept.
We use the extends
keyword to say that Type
has to have a length
property.
interface Length {
length: number
}
function itemLength<Type extends Length>(item: Type) {
return item.length
}
// `array` has .length ā
const pokemonArrayLength = itemLength(
['Bulbasaur', 'Charmander', 'Squirtle']
)
// `string` has .length ā
const singlePokemonLength = itemLength('Pikachu')
// Argument of type 'number' is not assignable to
// parameter of type 'Length'. š«
const numberLength = itemLength(1)
console.log(pokemonArrayLength) // 3
console.log(singlePokemonLength) // 7
console.log(numberLength) // undefined
The next example shows a sortPokemon
function that uses a generic constraint to constrain the shape of the pokemon
argument to the Pokemon
interface.
interface Pokemon {
name: string
hp: number
}
type Stat = 'hp'
function sortPokemon<Type extends Pokemon>(
pokemon: Type[],
stat: Stat
): Type[] {
return pokemon.sort(
(firstEl, secondEl) => secondEl[stat] - firstEl[stat]
)
}
const pokemon: Pokemon[] = [
{
name: 'Charmander',
hp: 39
},
{
name: 'Charmeleon',
hp: 58
},
{
name: 'Charizard',
hp: 78
},
]
// sort Pokemon by highest stat
console.log(sortPokemon(pokemon, 'hp'))
TypeScript generics are flexible. šŖ
Generic Constraints Using Type Parameters
In situations where we want to know if properties of an object we are trying to access exist we can use the keyof
operator.
Before I show you that letās first look at an example in JavaScript.
const pokemon = {
hp: 35,
name: 'Pikachu',
pokemonType: 'electric'
}
In JavaScript we can use Object.keys
to get the keys of an object.
Object.keys(pokemon) // ['hp', 'name', 'pokemonType']
We can do the type equivalent in TypeScript to make sure the property exists using the Key extends keyof Type
syntax.
The keyof
operator takes an object type and gives us a string or numeric literal union of its keys.
function getProperty<Type, Key extends keyof Type>(
obj: Type,
key: Key
) {
return obj[key]
}
const pokemon = {
hp: 35,
name: 'Pikachu',
pokemonType: 'electric'
}
// the property 'hp' exists on `pokemon` ā
getProperty(pokemon, 'hp')
// Argument of type '"oops"' is not assignable to
// parameter of type '"hp" | "name" | "pokemonType"'. š«
getProperty(pokemon, 'oops')
Enums
Enums are a set of named constants.
You might be familiar with enums from other languages. Enums are a feature of TypeScript and donāt exist in JavaScript.
In JavaScript itās popular to create an object with some constants to reduce the amount of typos.
const direction = {
up: 'UP',
right: 'RIGHT',
down: 'DOWN',
left: 'LEFT'
}
function movePokemon(direction) {
console.log(direction) // 'UP'
}
movePokemon(direction.up)
This is very useful when you have some dynamic code.
function movePokemon(direction) {
console.log(direction) // whatever got passed
}
movePokemon(direction[direction])
Enums are what you could use in such a case.
enum Direction {
Up = 'UP',
Right = 'RIGHT',
Down = 'DOWN',
Left = 'LEFT'
}
function movePokemon(direction: Direction) {
console.log(direction) // UP
}
movePokemon(Direction.Up)
TypeScript compiles Enums to some interesting š§ JavaScript code you can look at in the JavaScript output tab if youāre curious.
This looks like a fun š interview question.
var Direction
(function (Direction) {
Direction["Up"] = "UP"
Direction["Right"] = "RIGHT"
Direction["Down"] = "DOWN"
Direction["Left"] = "LEFT"
})(Direction || (Direction = {}))
You can learn more about enums since I just gave a brief overview.
Tuple
A tuple is an array with a fixed number of elements.
Use a tuple where the order is important.
type RGBColor = [number, number, number]
const color: RGBColor = [255, 255, 255]
You can specify optional values.
type RGBAColor = [number, number, number, number?]
const color: RGBAColor = [255, 255, 255, 0.4]
Cartesian coordinates anyone? šŗļø
type CartesianCoordinates = [x, y]
const coordinates: CartesianCoordinates = [3, 4]
Classes
Using TypeScript doesnāt mean you have to use classes. That being said letās explore what TypeScript adds to classes.
If youāre unfamiliar with classes you can read about Classes from the MDN Web Docs.
The readonly
member prevents assignments to the field outside of the constructor.
class Pokemon {
readonly name: string
constructor(name: string) {
this.name = name
}
}
const pokemon = new Pokemon('Pikachu')
// Cannot assign to 'name' because it is a read-only property. š«
pokemon.name = 'Charizard'
The public
member can be accessed anywhere and itās used by default, so you donāt have to type it unless you want to be explicit.
class Pokemon {
public name: string
constructor(name: string) {
this.name = name
}
}
const pokemon = new Pokemon('Pikachu')
pokemon.name = 'Charizard' // no problem ā
The protected
member is only visible to subclasses of the class itās declared in.
class Pokemon {
protected name: string
constructor(name: string) {
this.name = name
}
}
class LogPokemon extends Pokemon {
public logPokemon() {
console.log(this.name)
}
}
const pokemon = new LogPokemon('Pikachu')
// Property 'name' is protected and only accessible within
// class 'Pokemon' and its subclasses. š«
pokemon.name
// no problem ā
pokemon.logPokemon() // 'Pikachu'
The private
member is like protected
but doesnāt allow access to the member even from subclasses.
class Pokemon {
private name: string
constructor(name: string) {
this.name = name
}
}
class LogPokemon extends Pokemon {
public logPokemon() {
// Property 'name' is private and only
// accessible within class 'Pokemon'. š«
console.log(this.name)
}
}
const pokemon = new LogPokemon('Pikachu')
// Property 'name' is protected and only accessible within
// class 'Pokemon' and its subclasses. š«
pokemon.name
// no problem ā
pokemon.logPokemon() // 'Pikachu'
TypeScript gives us a shorter syntax for declaring a class property from the constructor using parameter properties by prefixing it with public
, private
, protected
, or readonly
.
class Pokemon {
name: string
constructor(name: string) {
this.name = name
}
}
class Pokemon {
constructor(public name: string) {
// no body necessary
}
}
You can implement š¤ an interface using the implements
keyword.
interface LogPokemon {
logPokemon(): void
}
class Pokemon implements LogPokemon {
constructor(public name: string) {}
public logPokemon() {
console.log(this.name)
}
}
class Pokedex implements LogPokemon {
// Property 'logPokemon' is missing in type 'Pokedex' but
// required in type 'LogPokemon'. š«
}
The syntax for a generic class is similar to a generic interface syntax.
class Pokemon<Type> {
constructor(public pokemon: Type) {}
}
// Pokemon<string> ā
const pokemonString = new Pokemon('Pikachu')
// Pokemon<string[]> ā
const pokemonArray = new Pokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
TypeScript introduces the concept of an abstract class thatās a contract used by other classes to extend from. The abstract class doesnāt contain implementation and canāt be instantiated.
abstract class Pokemon {
constructor(public name: string) {}
abstract useItem(item: string): void
}
class FirePokemon extends Pokemon {
useItem(item: string) {
console.log(`${this.name} used š§Ŗ ${item}`)
}
}
const charizard = new FirePokemon('Charizard')
charizard.useItem('Potion') // 'Charizard used š§Ŗ Potion.'
We glanced over classes in TypeScript because itās a large topic deserving itās own post.
If classes are something you want to learn more about using in TypeScript read the Classes section in the TypeScript Handbook.
In the next section weāre going to briefly introduce another concept that TypeScript introduced to classes ā decorators.
Decorators
Decorators are like higher-order functions (function that takes another function as an argument or returns a function) that can hook into a class and itās methods and properties, so we end up with composable and reusable pieces of code logic.
JavaScript has a proposal for decorators, so they might get added to the language in the future.
I want to preface this by saying that decorators are primarily used by library authors and frameworks such as Angular and NestJS.
Donāt sweat it when you would use decorators because theyāre mainly given to you to use them to improve your developer experience without having to think about how it works.
If youāre using decorators you have to enable experimental support for decorators inside tsconfig.json
.
{
"compilerOptions": {
"experimentalDecorators": true
}
}
In the next example weāre going to see how we can use a method decorator for validation to check if a Pokemon has enough experience to evolve by using @requiredExperience above the evolve
method.
The decorator implementation is inside requiredExperience
thatās also known as a decorator factory because it returns a function that will be called by the decorator at runtime.
function requiredExperience() {
return function(
target: any,
propertyKey: string,
descriptor: any
) {
// the original method
const originalMethod = descriptor.value
// overwrite the method
descriptor.value = function(...args: any[]) {
// if check passes...
if (this.experience > this.experienceThreshold) {
// use original method
originalMethod.apply(this, args)
} else {
// otherwise do something else
console.log(`${this.name} doesn't have enough experience to evolve into ${this.evolution}. š«`)
}
}
return descriptor
}
}
class Pokemon {
constructor(
private name: string,
private experience: number,
private evolution: string,
private experienceThreshold: number
) {}
@requiredExperience()
evolve() {
console.log(`${this.name} evolved to ${this.evolution}. āØ`)
}
}
const pikachu = new Pokemon('Pikachu', 80, 'Raichu', 120)
// "Pikachu doesn't have enough experience to
// evolve into Raichu." š«
pikachu.evolve()
We barely touched upon decorators because it would get lengthy and itās enough you just know about them.
Hooking into code like this is called metaprogramming and is often more useful in frameworks where you might need to add metadata to measure or analyze code.
Thereās a great video by Fireship on The Magic of TypeScript Decorators you should watch. šæ
If you want to dive deep into decorators you can read Decorators from the TypeScript Handbook.
Set Up TypeScript
Youāre probably learning TypeScript to use with a JavaScript framework and thatās great because most popular JavaScript frameworks require zero configuration for enabling TypeScript support. You should consult the docs for your framework how to set up TypeScript.
If you want to use vanilla TypeScript on a passion project I highly recommend using Vite. To set up your project just run npm init vite
and pick a template.
Letās look at how to set up TypeScript from scratch and show you around the TypeScript compiler settings.
Start by initializing a project inside an empty folder.
npm init -y
Install TypeScript as a development dependency.
npm i -D typescript
Next create a app.ts
file at the root of the project and add some TypeScript code.
const pokemon: string = 'Pikachu'
Since your web browser and Node donāt understand TypeScript we have to transpile the TypeScript code to JavaScript.
npx tsc app.ts
npm includes a npx
tool that runs executables without having to install a package globally. It just downloads the binary to your .bin
folder in node_modules
and removes it when youāre done.
The tsc
command invokes the TypeScript compiler that creates a app.js
file. This is what your browser and Node would run.
It would be a drag having to run this each time when you make a change, so you can pass a watch flag to the TypeScript compiler.
npx tsc app.ts -w
Itās easier to add a command to scripts in your package.json
.
{
"name": "typescript",
"scripts": {
"dev": "npx tsc app.ts -w"
},
"devDependencies": {
"typescript": "^4.3.5"
}
}
This lets us just run npm run dev
.
npm run dev
If youāre not using Vite and want live reload for your site you can use live-server together with TypeScript at the same time.
{
"name": "example",
"scripts": {
"dev": "live-server && npx tsc pokemon.ts -w"
},
"devDependencies": {
"typescript": "^4.3.5"
}
}
You only have to include the JavaScript file in your HTML file. Using the defer
attribute loads the JavaScript code after the DOM (Document Object Model) has loaded.
<!DOCTYPE html>
<html lang="en">
<head>
<title>TypeScript</title>
<script src="app.js" defer></script>
</head>
<body>
<h1>TypeScript</h1>
</body>
</html>
If youāre a Node chad šŖ you can use ts-node that lets you run TypeScript files without having to transpile them first by using ts-node app.ts
.
You also get a live code environment (REPL) if you run ts-node
like you would typing node
in your terminal.
If youāre using ts-node
you can use nodemon to watch the files by creating a nodemon app.ts
script in package.json
.
Tooling is a subject that deserves itās own post. I just want to expose you to what is out there.
Letās look at the TypeScript compiler options.
TypeScript looks for a tsconfig.json
file that we can create by hand or generate by using the --init
flag.
npx tsc --init
Inside the generated tsconfig.json
file we can see useful descriptions alongside the TypeScript compiler options.
You can look at the TypeScript compiler options and the TSConfig Reference that explains each option if you want to learn more.
Letās make the tsconfig.json
file more readable by removing most of the TypeScript compiler options and focusing on a couple of options.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["ESNext", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"noEmitOnError": true,
"strict": true
}
}
- target: specifies the target JavaScript version the TypeScript code compiles to where ESNext is the latest
- module: specifies the module system weāre going to transpile to where CommonJS uses the
const package = require('package')
syntax and the ES6 and onwards option uses ECMAScript modules (ESM)import package from 'package'
syntax which is supported natively in browsers only as of recent - lib: decides what types to include with our code because we might not need DOM types if weāre writing for Node or want to target some other JavaScript features (TypeScript includes a default set of type definitions for built-in JavaScript APIs)
- outDir: specifies the output directory for transpiled JavaScript code
- rootDir: specifies the root directory for TypeScript files
- noEmitOnError: doesnāt output anything if thereās a TypeScript error
- strict: enables all the strict flags so our code is more type-safe
Since we set the rootDir we donāt have specify what file to run inside package.json
since it watches the entire directory.
"scripts": {
"dev": "npx tsc -w"
}
If you want to fire up a quick TypeScript demo you can use an online editor like CodeSandbox so you can try things out for any project or framework.
Hope this helps you get started using TypeScript.
Reading Type Definitions
TypeScript gives us documentation inside our editor.
This is useful as code completion since you can dive into the types directly to understand the surface area of an API.
If you just started to learn TypeScript donāt be intimidated as it requires practice to understand the types first.
We can inspect the generic Array
interface by selecting Array
and pressing F12 or right-clicking it and selecting Go to Definition.
const pokemon: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']
Thereās always going to be a lot of information in type definition files, so focus on what youāre inspecting. You can jump from one type definition to another by doing the same to other types inside.
Thereās a couple of interesting things to note:
- The file ends with a
.d.ts
extension which is just a declaration file for types (itās used to add TypeScript types to things that arenāt built with TypeScript) - Thereās multiple type declarations on the right depending on the version of JavaScript the features were added in among other things (remember itās one interface since we can declare it again)
If we poke around lib.es2015.core.d.ts we can find the types that describe how to create arrays on the fly.
If we go back and look at lib.es5.d.ts on the right it looks like something we would expect. You can double-click it or Alt + Click the type definition to open it in a separate tab.
This has properties we would expect like length
, pop
, push
, and map
.
By looking at the type definitions we can learn how they typed the map
array method.
interface Array<T> {
// ...
map<U>(
callbackfn: (
value: T,
index: number,
array: T[]
) => U, thisArg?: any
): U[]
// ...
}
You can get a real idea how generics are used in practice.
Reading TypeScript Errors
Arguably, the hardest part about TypeScript can be reading and understanding errors.
First open the problems tab in your editor by pressing Ctrl + Shift + M so you have an easier time reading TypeScript errors.
const pokemon = {
bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}
const bulbasaur: string = 'bulbasaur'
// TypeScript is mad. š
āāļø
const chosenPokemon = pokemon[bulbasaur]
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ bulbasaur: { id: number; hp: number; attack: number; defense: number; }; charmander: { id: number; hp: number; attack: number; defense: number; }; squirtle: { id: number; hp: number; attack: number; defense: number; }; }'.
No index signature with a parameter of type 'string' was found on type '{ bulbasaur: { id: number; hp: number; attack: number; defense: number; }; charmander: { id: number; hp: number; attack: number; defense: number; }; squirtle: { id: number; hp: number; attack: number; defense: number; }; }'.
Sometimes youāre going to have these sort of errors that make you question your sanity when using TypeScript when itās a simple fix.
The error message is verbose because itās describing the entire object as a literal type since weāre not using a type alias or interface it can refer to instead.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Pokemon'.
No index signature with a parameter of type 'string' was found on type 'Pokemon'.
You can read TypeScript errors like sentences with because inbetween.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type ...
because no index signature with a parameter of type 'string' was found on type ...
The further you go down the because chain the more specific the error is, so start from there.
This problem might be harder to reason about if you donāt understand things like object index signatures tying everything we learned so far together.
The second part of the error message reveals the problem.
We donāt care if chosenPokemon
type is any
because thatās what TypeScript cares about ā we care about what causes it.
This is what TypeScript thinks is going on.
// TypeScript sees the general type `string` š«
const chosenPokemon = pokemon['random string']
// TypeScript expects the literal type `bulbasaur` ā
const chosenPokemon = pokemon['bulbasaur']
TypeScript freaks out because it thinks weāre passing some random string since we havenāt specified the index signature.
If you remember, objects are number indexed by default.
interface Pokemon {
[index: string]: {
id: number
hp: number
attack: number
defense: number
}
}
const pokemon: Pokemon = {
bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}
const bulbasaur: string = 'bulbasaur'
const chosenPokemon = pokemon[bulbasaur] // ok ā
That works, but itās not what we want.
We just want TypeScript to infer the type for us here so we donāt have to do it by hand, otherwise we would have to update the interface each time thereās another Pokemon.
interface Pokemon {
bulbasaur: {
id: number
hp: number
attack: number
defense: number
}
charmander: {
id: number
hp: number
attack: number
defense: number
}
squirtle: {
id: number
hp: number
attack: number
defense: number
}
}
The problem is that weāre not passing the type literal bulbasaur
, so TypeScript canāt compare it to the key bulbasaur
in pokemon
.
const pokemon = {
bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}
const bulbasaur: 'bulbasaur' = 'bulbasaur'
const chosenPokemon = pokemon[bulbasaur] // ā
Youāre going to encounter this when dealing with dynamic values.
If you used const
it would infer it as a literal type, so the type assignment isnāt required and itās only there so itās obvious.
Thereās times when TypeScript goes cuckoo for Cocoa Puffs and something goes wrong. Instead of closing and opening your editor just restart the TypeScript server by pressing F1 to open the command palette in VS Code and find TypeScript: Restart TS server.
If you donāt understand the error open up the TypeScript Playground and reproduce it there so you have a shareable link you can send to anyone.
You can always ask a question in the TypeScript Community Discord Server.
Dealing With Untyped Libraries
Almost every library you want to use supports TypeScript today.
The great thing about TypeScript is that the community can gather around to create types for libraries that donāt use TypeScript.
Ambient declarations describe the types that would have been there if the project was written in TypeScript and they have the .d.ts
file extension that TypeScript picks up.
DefinitelyTyped is a project that holds a repository for types that everyone gathered around to contribute types having over 80,000 commits and itās used to search for types on the official TypeScript page.
In the example we have a simple HTTP server in Node that sends a JSON response.
const http = require('http')
function requestListener(req, res) {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ pokemon: 'Pikachu' }))
}
const server = http.createServer(requestListener)
server.listen(8080)
You donāt have to use search to figure out if what youāre using is typed or not ā TypeScript is going to let you know.
To install a type definition you just have to do npm i -D @types/package
to install it as a development dependency.
Letās install the types for Node.
npm i -D @types/node
TypeScript lets us know we can convert require
to an import
.
import * as http from 'http'
You have to keep in mind these older libraries use CommonJS that uses module exports, so we canāt expect it to use ECMAScript modules where you can import a package simply as import http from 'http'
using the default export syntax or import { http } from http
syntax if itās a named import.
Try it first and if it doesnāt work you can use import * as http from 'http'
to import the entire moduleās contents.
You can brush up on how JavaScript modules work by reading JavaScript modules on the MDN Web Docs.
We still havenāt resolved the types for req
and res
for requestListener
since we canāt do that alone by just installing type definitions.
How do we figure the types in this case?
You can always search for the answer and thatās a valid approach but letās dig through the type definitions to figure it out.
Letās select the http
part and press F12 to Go to Definition. This is going to open the http.d.ts
type declaration file.
Inside the file we can do a Ctrl + F search for requestListener
. If that wouldnāt work we coud look for req
or res
until we find something.
We found our types! š The req
argument expects IncomingMessage
and the res
argument expects ServerResponse
.
We can import the types from the http
package.
import * as http from 'http'
import type { IncomingMessage, ServerResponse } from 'http'
The type
keyword is optional but it makes it clear weāre using types and not importing some method.
import * as http from 'http'
import type { IncomingMessage, ServerResponse } from 'http'
function requestListener(req: IncomingMessage, res: ServerResponse) {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ pokemon: 'Pikachu' }))
}
const server = http.createServer(requestListener)
server.listen(8080)
Now we have the entire http
API at our fingertips we can look at without leaving our editor.
Using types is easy in most cases when itās properly documented in a project that uses TypeScript. I wanted to show you how to deal with a scenario where that isnāt the case.
In the rare case when thereās no types for a package you can create a index.d.ts
file and place it inside a types
folder. The name could be anything.
declare module 'http'
This says to TypeScript the package exists and itās going to stop bothering you.
Generate Types
In case where we have some complex JSON object from an API response we can generate types from it to make our lives easier instead of typing it out by hand.
We can use quicktype to generate types for more than just TypeScript. Thereās also a quicktype extension for VS Code.
Their default example uses Pokemon, so itās perfect! Consistency. šŖ
This is also great as a learning tool.
Conclusion
You learned a lot about TypeScript fundamentals to give you confidence in using it in your projects. TypeScript itself is a tool that gives confidence about your code but donāt forget it doesnāt save you at runtime.
TypeScript is only gaining more popularity and since more projects are using TypeScript that means we as developers have to step up if we want to contribute to those projects.
Even if TypeScript fades and JavaScript gets types we didnāt have to learn another language to use types because TypeScript is JavaScript.
Thank you for your time! š