Typescript, an introduction

How to start writing Typescript in 15 minutes

article image
I've seen things you people wouldn't believe. Unless you are a Javascript developer, of course


Basic types

Primitives:

  • string
  • number
  • boolean

less used primitives:

  • bigInt
  • symbol

And others

  • null and undefined: different if "strictNullChecks" is on in the config file.
  • void: meaning the return can't be used anywhere else, must be disregarded.
  • any: just any type
  • Arrays: for arrays, two different syntax are available
string[] or Array<string> // for arrays with 0 or more string
[string] // exactly the same

And how do we type functions parameters and return value? Like this:

const happyBirthdayGreeting = (({ name }, age): ({ name: string }, number)): string => {...


And object properties and values? Like that:


const object: { name: string, age: number } = { name: 'sergio', age: 40 };

//you can declare conditional values:

{ name: string, age?: number } // remember, you'll have to check for undefined before using it:

if ( object.age !== undefined)

// or

object?.age


More complex types

Union types:

When the value might have more than one type

 string | number // it's one or the other. 
// * it is RESTRICTIVE, meaning that you can't use methods that are only allowing one of those two types. In order to make that work you'll have to use something like: 

if (Array.isArray(x))

// or

if (typeof name === 'string'), you know...


Type Aliases:

It's just an alias. Just to make it more readable and to share that type somewhere else

type Whatever = number | string

// and then you can use it somewhere else

const example: Whatever = 'whatever'

Interfaces:

It's another way to name an object type:

interface Point {
x: number,
  y: number
}


At this point you might be confused about the difference between interface and type, after all they serve a similar purpose. Here, the explanation:

here

Type assertions:

It is useful when you want to narrow down the type.

// ts only knows that it's a HTMLElement, but if we DO KNOW that it is going to be a canvas element
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

Another useful use case for assertions is when we want to set a type for a return:

const a = (expr as any) as T; when you want to set the type of something.

Literal types:

Whenever you want to be specific about the value:

const name: 'sergio' = 'pepe'. // error <-- it works for numbers, booleans, objects...

// you can mix literal types with other kind of types: eg: const mySomething = 'sergio' | number | myAlias


** Literal Inference: When you initialize a variable with an object, typescript assumes that the value of its properties might change of value later. So you might need to do something like this:

const req = { url: "https://example.com", method: "GET" as "GET" };
// or
const req = { url: "https://example.com", method: "GET" } as const;

void vs undefined

Kind of the same, but "void" must be disregarded:


this is valid:

const test = function(): undefined {
return;
}

const otherTest = function() {
if (!!test()) {
return 'is empty';
}
return 'is not empty';
}

this is not:

const test = function(): void {
return;
}

const otherTest = function() {
if (!!test()) {
return 'is empty';
}
return 'is not empty';
}

any vs unkown

"any" is for just anything. With unknown we have to narrow it down if we want to assign it to some other TYPE:

this from stackoverflow:

let vAny: any = 10;          // We can assign anything to 'any'
let vUnknown: unknown = 10; // We can assign anything to 'unknown' just as we do to a variable with 'any' type


let s1: string = vAny; // vAny is assignable to anything
let s2: string = vUnknown; // Invalid!!, we can't assign vUnknown to any other type (unless we use an explicit assertion)

vAny.method(); // ok anything goes with 'any'
vUnknown.method(); // NOT ok

example, fixed:

let vUnknown: unknown =  10;
let s2: string = vUnknown as string;

Narrowing

Introduction

TS is not going to let you call a method if that method is not available for ALL the types that can be.
In order to work with this, you must use "if"s 

example:

function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;

typeof type guards:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object" <---- include arrays and null, and obviously objects
  • "function"

The "in" operator narrowing

When you want to narrow by properties present in the object:

type Test1 = {
name: (name: string) => string
}

type Test2 = {
age: (age: number) => string
}

type Test = Test1 | Test2;

function test(p: Test): string {
if ('name' in p) return p.name('sergio')
return 'Jean Michelle'
}

test({ name: (name) => { return name }})

The "instance of" operator narrowing

Just what it sounds like. Very useful for classes:

class Car {

make: string;

constructor(make: string) {
this.make = make;
}
}
const auto = new Car('Honda');

function test(p: Car | string): string {
if (p instanceof Car) return p.make;
return `hellooooo I'm a string!!!`
}

test(auto)

Discriminated unions

Probably the easiest way to explain this is with an example:


This WON'T work

interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}

function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}

TS can't know that only when kind === 'circle', then 'radius' property is present.

So we do something like this:

interface Circle {
kind: "circle";
radius: number;
}

interface Square {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

function getAreaS(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
} else {
return null
}
}

// *it would work too using a "switch"

Tricks for Functions

Call Signatures

Because in Javascript functions are objects too ;P :

type DescribableFunction = {
description: string;
(someArg: number): boolean;
};

function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}

Constructor Signatures

Instantiating a Function? No problem!!

type SomeConstructor = {
new (s: string): SomeObject;
};

function makeHelloFunction(cntr: SomeConstructor) {
return new cntr("hello");
}

and if the function/object/whatever can instantiated with and without "new" -like Date, for example-:

interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}

Generic Functions

It creates a link between the different places where it is used in the function.

function firstElement<Type>(arr: Type[]): Type {
return arr[0];
}

// * note that we haven't created the type Type, it's inferred

This is the syntax for arrow functions:

const firstElement = <Type,>(arr: Type[]): Type => { 
return arr[0];
}

// or

const firstElement = <Type extends unknown>(arr: Type[]): Type => {
return arr[0];
}

Function Overloads

When you want to create functions that can accept a variable number of arguments. I'm not a huge fan of this.

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}

Tricks for Arrays

There is two types of array types: the normal ones and the tuple ones:

  • normal: Array<Type> or Type[]. Number of elements is flexible.
  • tuple: [number, number, string, Type]. Number of elements is fixed.

Arrays that can't be modified: "ReadOnlyArray<Type>"

const nameAndSurname: <string> = ['Sergio', 'Ibanez'];

Also "readonly" tuples:

const pair: readonly [string, number] = ['Loco', 8];

Use spreads with tuples the way that you would expect (Obviously order matters)

type StringNumberBooleans = [string, number, ...boolean[]]; // one string, then one number and then any number of booleans
type StringBooleansNumber = [string, ...boolean[], number]; // just what you'd expect

Using the spread operator might be tricky. TS doesn't assume that arrays are immutable, so you might have problems in cases like:

const args = [20, 44];
const angle = Math.atan2(...args); //error: "Expected 2 arguments, but got 0 or more"
So you have to do something like:
// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args)

Tricks for Objects

You can use either "interface" or an alias with "type" (up to you)

interface Person {
name: string;
age: number;
}
type Person = {
name: string;
age: number;
};

Optional properties

Works like this:

interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}

readonly

For properties that can't be reassigned:

interface Home {
readonly resident: { name: string; age: number };
}

Index signatures

In cases where the type of the key is unkown. Can be used in types and interfaces. The key can only be a string or a number.

type Killo = {
name: string,
age: number
}

type Test = {
[index: string]: Killo
}

const test: Test = {};

test.paisha = { name: 'sergio', age: 99 };

Indexes can also be "readOnly":

interface ReadonlyStringArray {
readonly [index: number]: string;
}

Extending types


Only interfaces extend ("aliases" do not, at least not using "extends", we'll see more about this later)

We can extend one:

interface Person {
name?: string;
age: number;
born: string;
country: string;
}


interface InterestingPerson extends Person {
isVampire: boolean;
}

or several:

interface InterestingPerson extends Person, Vampire {}

**types can't extend at all, but interfaces can extend other interfaces and types

Intersection types

Kind of the same that we have just done with interfaces but using types aliases instead:

type Colorful {
color: string;
}

type Circle {
radius: number;
}


type ColorfulCircle = Colorful & Circle;

Generic object types

Setting a type on the fly and using it.

interface Box<Type> {
contents: Type;
}


let boxA: Box<string> = { contents: "hello" };
let boxB: Box<number> = { contents: 5 };

you can use generics types with aliases as well:

type Box<Type> = {
contents: Type
}

All kinds of weird stuff

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

Classes

This are the basics

class Point {
x: number;
y: number;
}

const pt = new Point();
pt.x = 0;
pt.y = 0;

// or we can initialize it like this

class Point {
x: number = 0;
y: number = 0;
}

// or like this

class Point {
x: number;

constructor(value: number) {
this.x = value;
}
}

// readonly is available as well

class Point {
readonly x: number = 0;
y: number = 0;
}

methods

same syntax that functions

class Whatever {
myMethod(value: string): number {
if (value === 'mexico') return 10;
else return 0;
}
}

getters and setters

what you'd expect:

class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}

but there is some rules:

  • If set is not present, the property is automatically readonly
  • The type for the setter parameter is inferred from the return type of the getter
  • If the setter parameter has a type annotation, it must match the return type of the getter
  • Getters and setters must have the same Member Visibility (we will take a look at this now)

Methods (and properties) visibility

  • public: default one. Accesible from everywhere. Nothing to see here
  • protected: only visible to methods of subclasses of the class that the method is declared at.
class Parent {
protected myMethod() {
return 'sergio'
}
}

class Child extends Parent {
paisha() {
return this.myMethod() // valid
}
}

const killo = new Child()
killo.myMethod(); // this will throw an error
killo.paisha(); // this is fine
  • private: only visible and accessible by the class itself, not by subclasses:
class Parent {
private myMethod() {
return 'sergio'
}
}

class Child extends Parent {
paisha() {
return this.myMethod() // this will throw an error
}
}

const killo = new Child()
killo.myMethod(); // this will throw an error
killo.paisha(); // this will never happen :(

Index signatures

Works in the same way that in objects, but way cooler as we can use "this"

class MyClass {
[s: string]: boolean | ((s: string) => boolean);


check(s: string) {
return this[s] as boolean;
}
}

Implements

Check that a class satisfies a particular interface.


* The class needs to implement everything present in the interface. Also, the class may have extra properties and methods.

interface Id {
myName: () => string,
myAge: () => number
}

interface Flesh {
height: number,
}

class Human {
public name: string = 'sergio';
}

class Sergio extends Human implements Id, Flesh {

height: number = 187;

myName() {
return this.name;
}

myAge() {
return 40;
}

extraMethod() {
return 'whassssssup'
}
}

const killo = new Sergio()
killo.myName();

implements may have conditionals present

interface A {
x: number;
y?: number;
}

class C implements A {
x = 0;
y = 2;
}

const c = new C();
c.y = 10;

Generic classes

A class constructor can accept aliases in the contructor.

There is also other way, using generic classes, not a big fan. Both will work just fine, nevertheless I'd rather use the first one.

type Type = string;

class Box {
contents: SomeAlias;
constructor(value: SomeAlias) {
this.contents = value;
}
}

const b = new Box("hello!");

with generic classes:

class Box<Type> {
contents: SomeAlias;
constructor(value: SomeAlias) {
this.contents = value;
}
}

const b = new Box("hello again!");

"this" type

In classes, a special type called this refers dynamically to the type of the current class.

interface HaveName {
name: string,
}

class Parent {
_name: string = 'killo';

get name() {
return this._name;
}

doSomething() {
return this;
}
}

class Child extends Parent implements HaveName {
age: number = 50;
}

class Dog implements HaveName {
name: string;
good: boolean;

constructor(name: string, good: boolean) {
this.name = name;
this.good = good;
}
}

const bob: Child = new Child();
const john: Dog = new Dog('john', true);
const test: Child = bob.doSomething(); // Parent would work as well. Dog would error, though

"this is Type"

You can use this is Type in the return position for methods in classes and interfaces. Its basically in order to check if the instance is "instanceof" a certain class. And then you can narrow down like this:

class Parent {
isFirst(): this is FirstChild {
return this instanceof FirstChild;
}

isSecond(): this is SecondChild {
return this instanceof SecondChild;
}

}

class FirstChild extends Parent {
age: number = 50;
}

class SecondChild extends Parent {
address: string = 'wahtever';
}

const killo: Parent = new SecondChild();

if (killo.isSecond()) {
console.log(killo.address)
}

Abstract classes

An abstract class is a class that can't be instantiated but can be extended from.
Used to force inherited classes to implement the methods the abstract class.

abstract class AbstractParent {
abstract name: string;

myName() {
return 'my name is: ' + this.name;
}
}

class Child extends AbstractParent {
name: string = "sergio";
}

const killo: Child = new Child();
killo.myName(); // 'my name is sergio'

If you want to pass a parameter that is a class (!! NOT AN INSTANCE !!) which extends from an abstract class, must be done like this:

abstract class AbstractParent {
abstract name: string;

myName() {
return 'my name is: ' + this.name;
}
}

class Child extends AbstractParent {
name: string = "sergio";
}

const test = function(theClass: new () => AbstractParent) {
// you could use "typeof Child"
// but this is for all classes that inherit from that abstract class.
return new theClass().myName()
}

Final Trick

typeof operator can be used to reference the type of a value.

const point1 = { x: 10, y: 20 }
const point2: typeof point1 = { x: 55, y: 66 }

or just to create a new type:

const point1 = { x: 10, y: 20 }

type Point = typeof point1

const point2: Point = { x: 55, y: 66 }



Don't be shy, leave us a comment