
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:
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