Skip to main content

TypeScript

TypeScript is a superset of JavaScript, meaning all JavaScript is valid TypeScript, but not all TypeScript is valid JavaScript. It has been in the top 5 most loved languages in the StackOverflow developer survey for years.

Microsoft created TypeScript in 2010 because of the shortcomings of JavaScript. It is a statically typed language; types are known at compile time. This allows for better tooling and more robust code as it is easier to catch bugs early on.

Resources

When to use?

For many teams a no-brainer, for others a tough decision.

Here are some of the pros and cons to using TypeScript in your codebase:

ProCon
You are forced to write better code, that is easier to understand; reducing cognitive complexity.Comes at the cost of a small learning curve. You also end up writing and reviewing slightly more code.
ESLint rules based on the syntactic sugar of TypeScript lead to further improved code quality.Lends itself less well for prototyping.
Developers find out about bugs in their IDE instead of in productionSometimes refactors may be needed to align the signature of the code
Hiring and retention of developers is easier. Candidates sometimes lose interest when they hear you're not using TypeScriptNew developers might need more time to get up and running in case they haven't used TypeScript before.

Concepts

These are the things I picked up (over time) while learning TypeScript.

Types

Basic types

TypeScript has three basic types; number, string and boolean.

let myNumber: number
let myString: string
let myBoolean: boolean

JavaScript has no notion of integers, floats, doubles, etc. Therefor all numbers are number in TypeScript.

To illustrate this, let's compare TypeScript types with C# types:

C#stringcharboolobjectbytesbyteshortushortintuintdoublefloatlongulong
TypeScriptstringstringboolobjectnumbernumbernumbernumbernumbernumbernumbernumberbigintbigint

This is part of the reason the learning curve of TypeScript is so small.

Complex types

More complex types can be created by combining these basic types.

let myNumberArray: number[]

let myObject: {
name: string
age: number
isDeveloper: boolean
}

Special types

TypeScript has a few special types:

  • any: This type can be anything. It is the default type when no type is specified.
  • unknown: This type can be anything, but you have to check the type before using it.
  • never: This type can never be reached. It is used for functions that always throw an error or never return.
  • void: This type is used for functions that do not return anything.

Defining types

You can define types using type and interface.

  • Use interfaces to define the shape of objects.
  • Use type to define everything else.
type MyNamedNumber = number

interface Person {
name: string
age: number
}
info

Interfaces can be merged and extended, types can not.

interface Person {
name: string
}

interface Person {
age: number
}

interface Friend extends Person {
hobbies: string[]
}

const john: Friend = {
name: 'John',
age: 87,
hobbies: ['cards club', 'watching Twitch streams'],
}

Type usage

Using types in TypeScript is relatively lightweight. Most types are inferred and don't need to be set explicitly.

The most important places to use types are:

  • Function parameters
  • Function return types
  • To define the shape of objects
function add(a: number, b: number): number {
return a + b
}

interface Person {
name: string
age: number
}

function getPerson(): Person {
return { name: 'John', age: 42 }
}
Hidden complexity in JavaScript

Types solve hidden complexity in your code. Here is an oversimplified JavaScript example to illustrate implicit type conversion.

const double = (x) => {
return x + x
}

double(true) // returns 2
double('2') // returns '22'

In the real world, these bugs are often hidden in a function that is called deep in your code.

Union types

Union types are a way to define a variable that can be one of multiple types.

type Result = string | number | boolean

or more specific:

type Result = 'success' | 'error' | 'warning'

Map types

Using a map to represent a union type.

const performanceMap = {
low: 1,
medium: 2,
high: 3,
}

type Performance = keyof typeof performanceMap

Enums

Enums are a way to define a set of named constants. They are often used to define a set of options.

enum Color {
Red,
Green,
Blue,
}

let myColor: Color = Color.Green
Reverse lookup

Sometimes you may want to parse a number into an enum value and look up the name of that enum value.

enum Verbosity {
quiet = -1,
normal = 0,
verbose = 1,
veryVerbose = 2,
debug = 3,
}

const input: number = 2 // This value is read from CLI input or a config file

const verbosity = Verbosity[Verbosity[input]] // 2

if (verbosity > Verbosity.normal) {
console.log('Verbosity:', Verbosity[verbosity]) // Output: "Verbosity: veryVerbose"
}

Record

The Record type creates a new object with the given keys and values.

function getPerson(): Record<string, number> {
return { age: 42, ageTomorrow: 43 }
}

Type guards

Type guards are a way to check the type of variable at runtime.

function isNumber(value: unknown): value is number {
return typeof value === 'number'
}

Utility types

TypeScript has a few useful utility types.

Partial

The Partial type makes all properties of an object optional.

interface Person {
name: string
age: number
}

function getPerson(): Partial<Person> {
return { name: 'John' }
}

Required

The Required type makes all properties of an object required.

interface Person {
name: string
phoneNumber?: string // This property is normally optional
}

function setPerson(person: Required<Person>) {
// ...
}

Readonly

The Readonly type makes all properties of an object readonly.

This can be especially useful for immutable data structures.

interface Person {
name: string
age: number
}

function getPerson(): Readonly<Person> {
return { name: 'John', age: 42 }
}

Pick

The Pick type picks a subset of properties from an object.

interface Person {
name: string
age: number
isDeveloper: boolean
}

function getPerson(): Pick<Person, 'name' | 'age'> {
return { name: 'John', age: 42 }
}

Omit

The Omit type omits a subset of properties from an object.

interface Person {
name: string
age: number
isDeveloper: boolean
}

function getPerson(): Omit<Person, 'isDeveloper'> {
return { name: 'John', age: 42 }
}

Exclude

The Exclude type excludes a subset of types from a union type.

type MyType = string | number | boolean

function getPerson(): Exclude<MyType, boolean> {
return 42
}

Extract

The Extract type extracts a subset of types from a union type.

type MyType = string | number | boolean

function getPerson(): Extract<MyType, boolean> {
return true
}

Type assertions

Type assertions are a way to tell the compiler that you know more about the type than it does.

interface Person {
name: string
age: number
}

const john = { name: 'John' } as Person

TypeScript now thinks the object has age: number but it doesn't. This can lead to bugs in runtime.

Type assertions

Type assertions are often a sign of a design flaw in your code. Don't use them unless you have to.

Generics

Generics are a way to define types that are not known yet. They are often used in combination with arrays and promises.

async function getFirst<T>(array: T[]): Promise<T> {
return array[0]
}

Generics can add complexity to your code. Don't overuse them.

What are your thoughts?