Skip to content

09. Typescript

Carlos Jaramillo edited this page Oct 10, 2022 · 24 revisions

Typescript

TypeScript es un lenguaje de programación de código abierto, desarrollado y mantenido por Microsoft. Es un superconjunto(superset) de JavaScript, que esencialmente añade tipos estáticos y objetos basados en clases. Se transpila a JS (fácilmente usando parcel-bundler).

JS es dinamicamente y debilmente tipado.

Tipos básicos

  • boolean: Valor verdadero o falso.
let muted: boolean = true;
  • number: Números.
let numerador: number = 42;
  • string: Cadenas de texto.
let saludo: string = `Me llamo ${nombre}`;
  • string[]: Arreglo del tipo cadena de texto.
let people: string[] = [];
  • Funciones :void, :never.
const noReturn = ():void => {};
const noFinish = ():never => {}; // not finish because is a infinity loop or throw exception.
  • Union :number | string.
let age: number | string;
age = 20;
age = '20';

age.toString(); // execute shared methods between both types
// Union type remove the security of the object type but can recover it with TypeGuards

// TYPE GUARDS
// to take advantage of union types without losing the security of type.

// the syntax of returned type conver this function in type guard
function isNumber(obj: number | string): obj is number { 
     return typeof obj == 'number';
}

function printAge(age: number | string) {
    if(isNmber(age)) {
        // security about the object is number;
    } else {
        // heree typescript know (infer) is a string
        age.charAt(0);
    }
}
  • Intersection Types :User & admin.
class User {
    name: string;
}

class Admin {
    permissions: number;
}

let user: User & Admin;

user.name = 'Carlos';

user.permissions = 5; 

user = new User(); // error, because `user` is `User&Admin`

// TYPE ASSERTIONS
// is different from casting, you do not change the object, just override the compiler handle.

user = new User() as User&dmin;

// TYPE ALIASES
type numero = number;
let edad: numero;

type NumberOrString = number | string;
let age: NumberOrString;
  • Tuples :[string, number]. The difference with arrays, is the array only will accept one type
let tupla: [string, number];

// Error, because want string at [0] and number at [1]
tupla[0] = 20;
tupla[1] = '20';

tupla[2] = '1'; // this will accept union `string | number`
  • Array: Arreglo multi-tipo, acepta cadenas de texto o números.
// mutable
let peopleAndNumbers: Array<string | number> = [];
let peopleAndNumbers: (string | number)[] = [];

// inmutable
let peopleAndNumbers: ReadonlyArray<string | number> = [1, 'a', 'b'];

// const assertion
const config = ['http://hocalhost', 8000, '/api'] as const;

const config: readonly = ['http://hocalhost', 8000, '/api']; // cada item es readonly y su tipo es su propio valor
  • enum: Es un tipo especial llamado enumeración.
enum Color {
  First = "Rojo",
  Second = "Verde",
  Third = "Amarillo",
}

enum PaymentState { Creado, Pagado, EnDeuda }; // will assign 1, 2, 3
 
let colorFavorito: Color = Color.First;
  • any: Cualquier tipo.
let comodin: any  = "Joker";
comodin = { type: "WildCard" }
  • unknown: Cualquier tipo, pero debes revisar el tipo antes de ejecutar cualquier método.
const stringify = (value: unknown): string => {
  if (value instanfeof Date) {
    return value.toISOString();
  }
  if (Array.isArray(value)) {
    return JSON.stringify(value);
  }
}
  • object: Del tipo objeto.
let someObject: object = { type: "WildCard" };

type Config = {
  url: strin,
  port: number,
  path: string,
};

const config: Config = {
  url: 'http://localhost',
  port: 8000,
  path: '/api'
}

Funciones

function add(a: number, b: number): number {
  return a + b;
}
const sum = add(4, 25)

function createAdder(a: number): (number) => number {
  return function (b: number) {
    return a + b;
  }
}

type Double = (x: number) => number;
interface Double = { (x: number): number }

const double: Double = (x) => x * 2;
const double2: Double = function (x) {
  return x * 2;
}

Interfaces

interface Rectangulo {
  height: number,
  width: number
}

let rect: Rectangulo = {
  height: 4,
  width: 3
}

Genericos

function genericReceptor<T>(obj: T): T {
  return obj;
};

genericReceptor<string>('Carlos');
genericReceptor<number>(20);


// can be used in classes and interfaces
class Printer<T> {
  printAll(arr: T[]) {
    console.log(rr.length);
  }
}

let printer: Printer<number> = new Printer();
printer.printAll([12, 15]);


interface Asset {
 x, y: number;
}
function generic<T extends Asset>(obj: T) {};

generic<number>(20); // ERROR

class Point {
  x: number;
  y: number;
}
generic<Point>(new Point());


interface Asset<T> {
 x, y: number;
 generic: T;
}
function generic<T extends Asset<string>>(obj: T) {};

class Point implements Asset<string> {
  x: number;
  y: number;
  generico: string;
}
generic<Point>(new Point());


class Elements implements Asset<Point> {
  x: number;
  y: number;
  generico: Point;
}


const appendToArr = <T, U> (
  value: T,
  arr: ReadonlyArray<U>,
): ReadonlyArray<T | U> => [...arr, value];

const arr = ['a', 1]; // const arr: (strung | number)[];
const arr2 = appendToArr(false, arr) // const arr2: (strung | number | boolean)[];

Clases

JavaScript tradicional utiliza funciones y herencia basada en prototipos

class Greeter {
    greeting: string; // propertie

    // not return, only can exist 1
    constructor(message: string) {
        this.greeting = message;
    }

    greet() { // method
        return"Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

Clases Abstractas

Similares a las interfaces, pero estas si pueden tener implementación y las interfaces no. No pueden ser instanciadas, pero pueden tener métodos abstractos, que no deberían tener implementación si no que deben ser implementados por las sub-clases, como los metodos de las interfaces.

abstract class Greeter {
    x: number;
    y: number;
    getCoords(): string { return `${this.x},${this.y}` }

    abstract greet(name: string): string;
}

// Can't extends more classes.
class Hero extends Greeter {
    // Doesn't need implement x,y, getCoords
    greet(name: string) { return  `Hello ${name}` }
}

Herencia

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

/*
An alternative to this is implements Interface, 
some class only can extends one class/abstract class
but can implements multiples interfaces
*/
class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }

    /*
    // to override methods from parent
    move() {
      super.move(10); // to call methods from parent can use super.anyMethod();
    }
    */
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

Modificadores de Acceso

Encapsulación: Ocultar implementación de la funcionalidad y sólo exponer una forma de mandar a llamar la implementación. No deberíamos tener atributos publicos. Ahí entran los métodos accesores (Getter/Setters).

Esto no es por seguridad, ** analogía ** no tienes que saber cómo funciona el motor, sólo necesitas la interfaz * volante, pedales * todo lo demás sucede internamente. Para sólo exponer la interfaz, la forma de usar el objeto y no sus detalles. Para crear objetos independientes más fáciles de re utilizar.

Public (default)

class Animal {
    static url: string = 'http://google.com';
    /* 
    To access this just need to use `Animal.url` no need instance objects to access this. 
    Is used when the info is from the class and no from the object. Only need 1 copy of this variable. 
    Will access to this variable from the class instead of object. can create static methods too, 
    and to call it just need use `Animal.method()` and is used when method does not need internal state.
    */

    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

Private

No se puede acceder desde afuera de su clase.

class Animal {
    private _name: string;
    constructor(theName: string) { this._name = theName; }
    
    // Métodos accesores
    get name() { return this._name; }
    set name(name: string) { this._name = name; }
}

new Animal("Cat")._name; // Error: 'name' is private
new Animal("Cat").name; // Método accesor

Protected

Similar a private, añadiendo que las clases derivadas también pueden accederlo.

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name); // every time you override constructor need call super();
        this.department = department;
    }

    public getElevatorPitch() {
        return`Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

Namespaces

Para agrupar identificadores (clases, interfaces, constantes, simbolos) bajo un mismo nombre. Para evitar colisiones.

namespace MY {
    export class YouTube { };
    export const url: string = 'http://google.com';
    const private: string = '123';
}

let video: CF.YouTube = new CF.YouTube();

Decorators

Design pattern, where you get some property, class, or method, and you wrap it to add info or functionality. Want's to extend the functionality of some component without modifying it. To use with or without the decorator. in JS right now are in stage 2. But you can implement it with multiples strategies. Or with typescript because right now are experimentals, but angular use it a lot.

// get function who represents class constructor (is no the constructor function), 
// is the function who the object are createds. Because JS is prototype oriented and not classes. 
function auditClass(cls: Function) {
  cls.prototype.className = cls.name;
  console.log('decorator executed'); // is executed without instance the function
}

// clsProto: Hero or any, because usually decorators wants be generics.
function auditProperty(clsProto: any, propertyName: string) {
  clsProto.className = clsProto.constructor.name; // clsProto.constructor is the class
  console.log('decorator executed', propertyName);
}

function auditStaticProperty(cls: Function, propertyName: string) {
  cls.prototype.className = cls.name;
  console.log('decorator executed', propertyName);
}

// the descriptor is optional, because only get in target es5+ and will come with this props: https://www.javascripture.com/PropertyDescriptor
function auditMethod(clsProto: any, methodName: string, descriptor?: any) {
  let originalFunction = clsProto[methodName]; // property value or method decorated.

  let decoratedFunction = function() {
    originalFunction();
    console.log(`function ${methodName} executed`)
  }

  descriptor.value = decoratedFunction;
  return descriptor;
}

function auditMethodParam (clsProto: any, methodName: string, paramPosition: number) {
  clsProto.className = clsProto.constructor.name; // clsProto.constructor is the class
  console.log('decorator executed', methodName, paramPosition);
}

// decorator factory // all decorators can implement factory to get arguments
function Auditable (message: string) {

  return function auditMethod(clsProto: any, methodName: string, descriptor?: any) {
    let originalFunction = clsProto[methodName];

    let decoratedFunction = function() {
      originalFunction();
      console.log(`message`)
    }

    descriptor.value = decoratedFunction;
    return descriptor;
  }
}

@auditClass
class Hero() {
  // all decorators can be used on top or before some component.
  @auditProperty title: string;

  @auditStaticProperty 
  static site: string;

  @auditMethod
  greet(@auditMethodParam example: string) { console.log('Hi`) }

  @Auditable('Tracking')
  thanks() { console.log('Thanks`) }
}

let hero: Hero = new Hero();
console.log(hero.className); // ts error - doesn't exist because decorators works in execution time
console.log((hero as any).className);
hero.greet();
hero.thanks();

tsconfig.json

before es6 need use a specific module handler

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "removeComments": true,
    "outDir": "dist",
    "noImplicitAny": true,
    "sourceMap": true,
    "watch": true,
    "rootDir": "src",
    "experimentalDecorators": "true"
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

Typescript Basics

Type Erasure

The types are used for type checking, then erased from the compiled output.

Tuples

TypeScript supports tuples, which are arrays of fixed length. For example, [number, number] is a tuple of two numbers

let numbers: [number, number] = [1]; // type error
let numberAndString: [number, string] = [1, 'a'];

Type unions

Literal Types

We've seen basic types like number and string. Every concrete number and every concrete string is also a type.

let one: 1 = 1; // 1
let oneOrTwo: 1 | 2 = 2; // 2
let one: 1 = 2; // type error

Syntax errors vs type errors

  • JavaScript: syntax check -> execution.
  • TypeScript: syntax check -> type check -> execution.

What's the difference between a syntax error and a type error? We can think of it by analogy to English. "Vase triangle avocado cat grape" is not valid English syntax. Each of those words is part of English, but they can't be in that order. No English sentence can be made of five nouns in a row.

Generic function types

function first<T>(elements: Array<T>): T {
  return elements[0];
}

type First<T> = (elements: Array<T>) => T;

const firstString: First<string> = first;

Generic identity function

An identity function is one that returns its argument without changing it.

Type Guard

a type guard makes a guarantee about the type. It "guards" the code inside the conditional, ensuring that it only executes for certain types.

function nameOrLength(userOrUsers: User | User[]) {
  if (Array.isArray(userOrUsers)) { // Type guard
    // Inside this side of the if, userOrUsers' type is User[].
    return userOrUsers.length;
  } else {
    // Inside this side of the if, userOrUsers' type is User.
    return userOrUsers.name;
  }
}

In both cases, we say that we've narrowed the type. We've taken a wide type like User | User[] and reduced it to a narrower type like User or User[].

Object narrowing - (Duck typing)

We can assign object types to "smaller" object types. This is called narrowing: we narrow the "larger" type to the "smaller" type. For example, we might have a full user type:

type User = {
  email: string
  admin: boolean
};
let amir: User = {
  email: '[email protected]',
  admin: true,
};
amir.email; // '[email protected]'

type HasEmail = {
  email: string
};
let amirEmail: HasEmail = amir;

amirEmail.email; // '[email protected]'
amirEmail.admin; // type error

function sendEmail({email}: {email: string}): string {
  return `Emailing ${email}`;
}

[
  sendEmail(amir),
  sendEmail({email: '[email protected]'}),
]; // ['Emailing [email protected]', 'Emailing [email protected]']

This is called structural typing: the object's structure determines the type. Some other programming languages would require us to explicitly say "a User is a bigger kind of HasEmail." That would be nominal typing and is not supported by TypeScript.

Finally, a note on terminology. Structural typing is also sometimes called "duck typing". This term is a reference to a saying: "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."

In programming, "duck typing" means that we only care about the properties we're accessing. We don't care whether the type's name is User or HasEmail, and we don't care whether the object has extra properties.

Nullability

let mightBeAString: string | undefined = true ? 'a' : undefined;
let s: string;

/**
 * with `--strictNullChecks` option changes type checking for `null` and `undefined`
**/
let s: string = mightBeAString; // type error: Type 'string | undefined' is not assignable to type 'string'.
s = mightBeAString === undefined ? '' : mightBeAString // 'a'

Undefined in arrays

let strings: string[] = ['a'];
let element: string = strings[5];
element; // undefined

const s: string = ['a'][1];
s; // undefined

const numbers = [1, 2, 3];
const n = numbers[100];
/**
 * As an alternative we can enable the `--noUncheckedIndexedAccess`
 * With this option enabled, accessing numbers[i] gives us a number | undefined
 * This forces us to check for undefined whenever we access an array element by its index, which is more annoying but also more safe.
 **/
const isUndefined = n === undefined ? 'yes' : 'no';
isUndefined; // 'yes'

String to a number in TS

/**
 * Any operation on a NaN returns another NaN.
 * Unfortunately, that means that NaNs will propagate through our code, with every operation on them returning yet another NaN.
 **/
function stringToNumber(s: string): number | undefined {
    // (parseFloat('a3') + 1 * 20) / 2 = NaN
  const n = parseFloat(s);
  if (isNaN(n)) return undefined;
  return n;
}

// Type safety when parsing numbers

stringToNumber('3') + 1; // type error: Object is possibly 'undefined'.

const n = stringToNumber('3')
n !== undefined ? n + 1 : undefined // 4