Typescript

# O que é?

  • Um superset do Javascript (adiciona features ao Javascript)
  • Implementa tipagem estática (definição de váriaveis)
  • Compila para JS (exceto no Deno)
  • Intercambiável com JS (arquivos com extensões .ts e .js)
  • Mesma sintaxe do JS
  • Pode inferir tipos

# Por que?

  • Evita resultados inesperados (como soma de int e string)
  • Avisa se estiver fazendo algo errado
  • Já funciona como uma espécie de documentação
  • Melhora o autocomplete do editor de código (ex: mostra todos os métodos de um objeto)
  • Pode substituir o JS gradualmente
  • Facilita os testes

# Desvantagens

  • Precisa ser compilado (exceto no Deno)
  • Erros nem sempre são muito claros ou são muito grandes

# Como instalar

No terminal, rode o comando: npm install -g typescript

# Como iniciar projeto

No terminal, rode o comando: tsc --init

# Como rodar um projeto

No terminal, rode o comando: tsc --watch e use o nodemon ou node para executar os arquivos js.

# Tipos

  • Boolean (true - false)
  • String ('foo', "foo", foo)
  • Number (50, 10.1, 0xff0)
  • Array (string[ ], number[ ], object[ ], Array, Array...)
  • Tuple ( [number, string, number] )
  • Enum
  • Any (comportamento padrão do JS, não use)
  • Void (funções sem retorno, como console.log)
  • Null (type Bla = string | undefined)
  • Undefined (semelhante ao null)
  • Never (nunca retorna, usa-se por exemplo, ao jogar um erro, pois a execução é parada)
  • Unknown (semelhante ao any, mas não permite que nenhuma propriedade ou método da váriavel seja modificado)
  • Object

# Inferência de tipos (type inference)

Se você declara uma váriavel já com um valor atribuído, o typescript define automaticamente o tipo da variável, que não poderá ser alterado posteriormente.

let msg = "essa é uma string";
msg = "string nova"; // funciona
msg = 10; // não funciona

# União (Union)

Permite que uma váriavel possa aceitar mais de um tipo:

let id: number | string;
id = 10; // funciona
id = "abc"; // funciona
id = true; // não funciona

# Type Alias

Permite criar um tipo personalizado. Ao invés de usar o union pode-se criar um tipo:

type Uid = number | string;
let id: Uid;

id = 10; // funciona
id = "abc"; // funciona
id = true; // não funciona

Com o type alias também se pode definir os valores que são aceitos por uma váriavel:

type Platform = 'Windows', 'Linux';
let system: Platform;

system = 'Windows'; // funciona
system = 'MacOS'; // não funciona

Também consegue-se definir tipos mais complexos, como objetos:

type AccountInfo = {
	id: number;
	name: string;
	email?: string; // Opcional
}

const account: AccountInfo = {
	id: 1,	
	name: "John"
}

# Intersecção (Intersection)

Permite unir type aliases:

type CharInfo = {
	nickname: string;
	level: number;
}

type PlayerInfo = AccountInfo & CharInfo;

const player: PlayerInfo = {
  id: 123,
  name: "William",
  nickname: "will",
  level: 10,
};

É possível usar o intersection diretamente na atribuição.

type PlayerOne = AccountInfo & CharInfo & { content: boolean };

# Classes

Criando uma classe:

class UserAccount {
  public name: string;
  protected age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  logDetails(): void {
    console.log(`The player ${this.name} is 
									${this.age} years old`);
  }
}

Extendendo uma classe e adicionando propriedades e métodos:

class CharAccount extends UserAccount {
  private nickname: string;
  readonly level: number;

  constructor(name: string, age: number, 
							nickname: string, level: number) {
    super(name, age);

    this.nickname = nickname;
    this.level = level;
  }

  logCharDetails(): void {
    console.log(`The player ${this.name} 
    has the char ${this.nickname}
    at level ${this.level}`);
  }
  changeNickname(nickname: string): void {
    this.nickname = nickname;
  }
	changeAge(age: number): void {
    this.age = age;
  }
}

# Modifiers

Private: impede que uma propriedade seja modificada do lado de fora da classe. Na classe acima, a propriedade nickname está como private, assim se criarmos um objeto com base nessa classe e tentarmos alterar ou visualizar a propriedade nickname sem usar um método da própria classe, não irá funcionar.

const john = new CharAccount("John", 45, "JohnNoobKiller", 1500);

john.nickname = "jonjon"; // não funciona
console.log(john.nickname); // não funciona
john.changeNickname("ItSJonny"); // funciona

Readonly: impede a modificação da propriedade, mesmo que por um método de dentro da própria classe, mas permite a visualização de uma propriedade tanto dentro quanto do lado de fora de sua classe.

john.level = 10; // não funciona
console.log(john.level); // funciona

Public: pode-se realizar qualquer operação com a propriedade mesmo que seja realizada fora da classe, por padrão, todas as propriedades e objetos criados na classe são públicos.

john.name = "Jonatas"; // funciona
console.log(john.name); // funciona

Protected: semelhante ao private, não permite nem a visualização e nem modificação do lado de fora da classe, mas permite que além da própria classe, suas subclasses também possam alterar a propriedade.

john.changeAge(50) // funciona
john.age = 10; // não funciona

# Accessors

Considerando a seguinte classe:

class CharAccount extends UserAccount {
  private nickname: string;
	// level alterada para public, para permitir modificação
  public level: number; 

  constructor(name: string, age: number, nickname: string, level: number) {
    super(name, age);

    this.nickname = nickname;
    this.level = level;
  }
	
	// getter
  get getLevel() {
    return this.level;
  }

	// setter
	set setLevel() {
    this.level = level
  }
}
const john = new CharAccount("John", 45, "JohnNoobKiller", 1500);

# Getters (get)

É uma propriedade da classe que retorna um valor de uma propriedade, semelhante a um método.

console.log(john.getLevel); // printa 1500

# Setter (set)

É uma propriedade da classe que permite a alteração de um valor de uma propriedade.

john.setLevel = 40;
console.log(john.getLevel); // printa 40

# Abstract Class

É uma classe que não permite a criação de objetos a partir dela.

abstract class EletronicDevice {
  public productName: string;
  public company: string;
  public year: number;

  constructor(productName: string, company: string, year: number) {
    this.productName = productName;
    this.company = company;
    this.year = year;
  }

  get getProductName() {
    return this.productName;
  }

  get getCompany() {
    return this.company;
  }

  get getYear() {
    return this.year;
  }
}
const tv = new EletronicDevice("TV", "LG", 2000); // não funciona

Mas pode-se criar novas classes à partir dela.

class Videogame extends EletronicDevice {
  cpu: string;
  gpu: string;
  memorySize: number;

  constructor(
    productName: string,
    company: string,
    year: number,
    cpu: string,
    gpu: string,
    memorySizeInMB: number
  ) {
    super(productName, company, year);
    this.cpu = cpu;
    this.gpu = gpu;
    this.memorySize = memorySizeInMB;
  }

  get getMemorySize() {
    return this.memorySize;
  }
}

E agora, podemos criar um novo objeto à partir da classe Videogame.

const PlayStation4 = new Videogame(
  "Playstation4",
  "Sony",
  2013,
  "AMD",
  "Radeon",
  8000
);

console.log(
  `A fabricante do ${PlayStation4.name} é ${PlayStation4.getCompany}, 
		e ele possui ${PlayStation4.getMemorySize} MB de ram`
);

# Interfaces

Funciona como o type alias, mas é usado somente para descrição de objetos mais complexos.

interface Game {
  title: string;
  description: string;
  genre: string;
  platform?: string[]; // opcional
  getSimilars?: (title: string) => void; // opcional
}

Podemos criar uma variável com esse tipo.

const tlou: Game = {
  title: "The Last of Us",
  description: "Top",
  genre: "Action",
  platform: ["PS3", "PS4"],
  getSimilars: (title: string) => {
    console.log(`'${title}' similars: Uncharted`)
  }
}

Também podemos determinar, como nas classes, se uma propriedade pode ser alterada ou não, com os modifiers.

interface VideoGame {
  name: string;
  readonly price: number;
}

const ps4: VideoGame = {
  name: "Playstation 4",
  price: 1500
}
ps4.price = 1550; // não funciona, pois 'price' é readonly

E definir se uma propriedade pode ser opcional.

interface PC {
	cpu: string;
	gpu: string;
	ram: number;
	mouse?: string; // opcional
  keyboard?: string; // opcional
  printerIsConnected?: (printerName: string) => void;
}
const myPC: PC = {
  cpu: 'intel', gpu: 'amd', ram: 8000, 
  printerIsConnected: (printerName: string) => {
    console.log(`Printer ${printerName} is connected!`)
  }
} // funciona

Se definirmos um método como opcional e precisarmos referenciá-lo, é necessário primeiramente colocar um if para checar se ele existe.

console.log(myPC.printerIsConnected("hp")) // erro, pois o método pode ser undefined
if (myPC.printerIsConnected) { myPC.printerIsConnected("hp"); } // funciona

Podemos, como nas classes, criar uma interface à partir de outra, herdando suas propriedades.

interface Server extends PC {
  hardDisksAmount: number;
  raidIsTurnedOn:  (disksId: number[]) => void;
}
const myServer: Server = {
  cpu: 'amd', gpu: 'amd', ram: 2000, hardDisksAmount: 3, 
  raidIsTurnedOn: (disksId: number[]) => {
    console.log(`Raid is turned on at disks ${disksId}`)
  } 
}

É possível ainda implementar uma classe baseada em uma interface.

interface Game {
  title: string;
  description: string;
  genre: string;
  platform?: string[];
  getSimilars: (title: string) => void;
}

class CreateGame implements Game {
  title: string;
  description: string;
  genre: string;

  constructor(t: string, d: string, g: string) {
    this.title = t;
    this.description = d;
    this.genre = g;
  }
}

# Interfaces vs Type Alias

# Definição:

type Game = {
  title: string;
}

type Dlc = {
	extra: string;
}
interface Game {
  title: string;
}

interface Dlc {
extra: string;
}

# Intersecção / Extensão


type GameCollection = Game & Dlc;

interface GameCollection 
extends Game, Dlc {
};

# Implementando uma classe

class CreateGame 
implements GameCollection {}
class CreateGame 
implements GameCollection {}

# Declarando função

type getSimilars = 
(title: string) => void;

interface getSimilars {
	(title: string): void;
}

# Diferenças

Type Alias permite declarar tipos primitivos, interfaces não.

type ID = string | number;

interface ID extends number{} 
// não funciona

Type Alias permite declarar tuplas, interfaces não.

type tupleA = [number, number];
[1, 2] as tupleA; // aceita
[1, 2, 3] as tupleA; // não aceita

interface tupleB {
	0: number;
	1: number;
}

[1, 2, 3, "tabula"] as tupleB; 
// aceita, mas não deveria

Type Alias permite somente uma declaração por escopo, protegendo o código contra redeclaração. Já as interfaces permitem várias declarações e une as de mesmo nome, permitindo a extensão do código, sendo útil na criação de bibliotecas.

type JQuery = { a: string };
type JQuery = { b: string };
// não funciona, gera erro

interface JQuery {
	a: string;
}
interface JQuery {
	b: string;
}
const jq: Jquery = {
	a: "foo",
	b: "bar"
}
// funciona

# Quando usar Type Alias ou Interfaces?

Interfaces:

  • Quando estiver criando bibliotecas.
  • Quando precisar que o código seja extensível.
  • Quando estiver criando algo mais orientado à objetos.

Type Alias:

  • Quando estiver criando algo mais simples.
  • Quando precisar criar um alias para tipos primitivos.
  • Em quase todos os casos.

Comece utilizando o type alias e se precisar extender troque para interfaces, mas mantenha a consistência do código.

# Generics

É usado para bloquear a mudança de tipos de uma variável de maneira dinâmica, não sendo necessário definir previamente quais os tipos serão aceitos, somente no momento da inicialização da variável.

function useState<S>() {
  let state: S;

  function getState() {
    return state;
  };

  function setState(newState: S) {
    state = newState;
  };

  return { getState, setState };
}

const newState = useState<string>();

newState.setState(123); // não aceita
newState.setState("foo"); // aceita

Pode-se restringir os tipos que a função pode aceitar.

function useState<S extends number | string>() {
  let state: S;

  function getState() {
    return state;
  };

  function setState(newState: S) {
    state = newState;
  };

  return { getState, setState };
}

const newState = useState<string>(); // aceita
const newState = useState<boolean>(); // não aceita

Pode-se também definir qual será o tipo padrão, se não declarado na inicialização da variável.

function useState<S extends number | string = number>() {
  let state: S;

  function getState() {
    return state;
  };

  function setState(newState: S) {
    state = newState;
  };

  return { getState, setState };
}

const newState = useState(); // não é necessário declarar o tipo
newState.setState(123); // aceita
newState.setState("foo"); // não aceita

Pode-se combinar type alias e interfaces com generics.

type numOrStr = number | string;

function useState<S extends numOrStr = number>() {
  let state: S;

  // [...]

  return { getState, setState };
}

# Onde se usa generics?

  • Hooks
  • Criação de componentes React. Ex: React.FC<Props, States>

# Type Utilities

Usado para realizar modificações nos tipos.

  • Readonly (só permite leitura das propriedades)
type Todo = {
  title: string;
  description: string;
  completed: boolean;
}

const todo: Readonly<Todo> = {
  title: "Assistir Dark",
  description: "Relembrar detalhes",
  completed: false
};

todo.completed = true; // não funciona, pois é readonly
  • Partial (torna todos as propriedades opcionais)
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return {
    ...todo, ...fieldsToUpdate
  }
}

const todo2: Todo = updateTodo(todo, 
  { title: "Assistir Futurama", completed: true })
  • Pick (seleciona todas as propriedades que você quer de um tipo, você precisa dizer as que quer)
type TodoPreview = Pick<Todo, "title" | "completed">

const todo3: TodoPreview = {
  title: "Zerar GTA SA de novo",
  completed: false
}

const todo4: Pick<Todo, "title"> = {
  title: "Comprar feijão"
}
  • Omit (seleciona todas as propriedades de um tipo, exceto as que você não quer, você precisa dizer as que não quer)
type TodoPreview2 = Omit<Todo, "description">

const todo5: TodoPreview = {
  title: "Zerar GTA SA de novo",
  completed: false
}

# Decorators

Modifica o comportamento de propriedades.

  • Class Decorator
function setAPIVersion(apiVersion: string) {
  return (constructor) => {
    return class extends constructor {
      version = apiVersion
    }  
  }
}

@setAPIVersion("1.0.1")
class API {}
  • Property Decorator
function minLength(length: number) {
  return (target: any, key: string) => {
    let val = target[key];

    const getter = () => val;

    const setter = (value: string) => {
      if(value.length < length) {
        console.log(`Error: ${value} tem menos que ${length} letras`)
      } else {
        val = value;
      }
    } 
    Object.defineProperty (target, key, {
      get: getter,
      set: setter
    })
  }
}

class Movie {
  // validação - erro se menor que 5 letras 
  @minLength(5)
  title: string;

  constructor(title: string) {
    this.title = title;
  }
}

const movie = new Movie("Int"); // Int tem menos que  letras
  • Method Decorator
function delay(ms: number) {
  return (target: any, key: string, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args) {
      console.log(`Esperando ${ms/1000} segundos...`)
      setTimeout(() => {
        originalMethod.apply(this, args);
      }, ms);
      
      return descriptor;
    }
  }
}

class Greeter {
  greeting: string;

  constructor(g: string) {
    this.greeting = g;
  }

  // esperar um tempo antes de rodar o método
  @delay(2000)
  greet() {
    console.log(`Hello! ${this.greeting}`)
  }
}

const littlePerson = new Greeter("Pessoinha")
littlePerson.greet();
// Esperando 2 segundos...
// Hello! Pessoinha
  • Parameter Decorator
  • Acessor Decorator