Typescript 101 - [2]

Não sei criar tipos pra N objetos, e agora?

Introdução

Fala aí galera, tranquilos? Eu demorei pra lançar esse artigo pois queria construir algo com bastante tipagem complexa para conseguir fazer um deep dive em TS. Sem mais delongas, vamos lá

Generics - Inferindo os tipos de qualquer lugar

Generics é uma técnica interessante para que possamos trabalhar com um tipo que atenda a uma todos os tipos que satisfação a sua condição de uso. Os generics vão ser por padrão um tipo não estabelecido e não iterável (significa que você precisará informar quando um tipo genérico for um Array).

Beleza, mas quando eu vou usar isso?

type type Arrays<T> = T[]Arrays<function (type parameter) T in type Arrays<T>T> = function (type parameter) T in type Arrays<T>T[];

const const a: Arrays<string>a: type Arrays<T> = T[]Arrays<string> = [];

Simples pra você começar a entender. A variável a será forçada a ser um array de string. No tipo Arrays nós recebemos um genérico através do <T> para que possamos operar em um tipo que não conhecemos, mas que será inferido pelo nosso tipo ao receber o seu "alvo".

Generics é uma poderosa forma de criar tipos com base nos nossos objetos, arrays ou até em primitivos. Vou fazer alguns exemplos do médio ao avançado para você poder conferir. Lembre-se, você pode usar o playground para fazer testes rápidos ao invés de configurar um arquivo local.

Utility Types - Readonly

Utility Types são tipos builtin do Typescript para que você possa criar seus tipos com uma ajudinha extra. Nessa parte irei falar do tipo readonly. Iremos usa-lo para impedir reatribuição de valores numa função

const map = <T>(a: Readonly<T[]>, newValue: T) => {
  // Index signature in type 'readonly T[]' only permits reading
  // Não podemos reatribuir valores do nosso array, graças ao Readonly
  a[0] = newValue;
};

Você também pode usar o Readonly para definir o tipo dos seus objetos como imutáveis e impedir que os mesmos sejam reatribuídos.

type User = Readonly<{
  name: string;
  birthDate: Date;
  skills: string[];
}>;

const user: User = {
  name: "Joãozinho",
  birthDate: new Date(),
  skills: ["Contar piadas"],
};

// Error
user.name = "Fuba";

E antes que eu esqueça, todos os tipos builtin do Utility Types são tipos que fazem o uso de generics.

Redux Action

Momento de puxar para o lado do React. Se você nunca programou, não tem problema, vou fazer um exemplo bem comum

enum enum ActionsTypesActionsTypes {
  function (enum member) ActionsTypes.Open = "Action/Open"Open = "Action/Open",
  function (enum member) ActionsTypes.Close = "Action/Close"Close = "Action/Close",
}

// Aqui usamos o generics para concatenar com o objeto padrão das actions recebidas no reducer
// Com o `= {}` forçamos que o recebido da nossa função seja um objeto, evitando tipos errados
type 
type Action<T = {}> = {
    type: ActionsTypes;
} & T
Action
<function (type parameter) T in type Action<T = {}>T = {}> = { type: ActionsTypestype: enum ActionsTypesActionsTypes } & function (type parameter) T in type Action<T = {}>T;
const
const initialState: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
initialState
= {
loading: booleanloading: false, user: nulluser: null, authorized: booleanauthorized: false, }; // O uso do nosso Action<T> fica transparente e facilita na tipagem das ações de nosso reducer type
type AuthActions = {
    type: ActionsTypes;
} & Partial<{
    login: string;
    mock: null;
}>
AuthActions
=
type Action<T = {}> = {
    type: ActionsTypes;
} & T
Action
<
type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
Make all properties in T optional
Partial
<{
login: stringlogin: string; mock: nullmock: null; }> >; const
const authReducer: (state: {
    loading: boolean;
    user: null;
    authorized: boolean;
} | undefined, action: AuthActions) => {
    loading: boolean;
    user: null;
    authorized: boolean;
} | {
    login: string | undefined;
    loading: boolean;
    user: null;
    authorized: boolean;
}
authReducer
= (
state: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
state
=
const initialState: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
initialState
, action: AuthActionsaction:
type AuthActions = {
    type: ActionsTypes;
} & Partial<{
    login: string;
    mock: null;
}>
AuthActions
) => {
switch (action: AuthActionsaction.type: ActionsTypestype) { case enum ActionsTypesActionsTypes.function (enum member) ActionsTypes.Close = "Action/Close"Close: return { ...
state: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
state
, login: stringlogin: "" };
case enum ActionsTypesActionsTypes.function (enum member) ActionsTypes.Open = "Action/Open"Open: return { ...
state: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
state
, login: string | undefinedlogin: action: AuthActionsaction.login?: string | undefinedlogin };
default: return
state: {
    loading: boolean;
    user: null;
    authorized: boolean;
}
state
;
} };

Hack PromiseAll

Esse exemplo foi recente. Tive um problema com um Promise.all que possuia mais de 10 itens, e sua definição possui suporte somente até 10 itens. Tive que fazer uma rataria pra fazer funcionar no meu caso. Mas para isso, tive que obrigar algumas coisas para que a tipagem funcionasse.

Obs: {"PromiseLike<T>"}: O meu tipo poderia ou não ser uma promise. Esse tipo foi retirado da definição oficial de Promise

  1. Cada item da minha promise deveria ser readonly para que os tipos pudessem ser tratados como constantes/imutáveis.
  2. O array passado para minha função PromiseAll deverá ser passado como as const para garantir o readonly.
  3. Foi usado ...values: ReadonlyPromise<T>[] e values[0] foram usados para "trapacear" a tipagem original, assim como o as any no Promise.all e após a invocação do método.

Para você não ficar viajando, vou explicar o que é cada tipo antes de você ler o código:

  • {"Unwrap<T>"}: Esse tipo irá fazer testes no tipo para verificar se o mesmo é uma Promise e o resolve, funcionando mais ou menos como um await
  • {"ReadonlyPromise<T>"}: Garantindo que o meu tipo seja Readonly ou seja um Readonly de PromiseLike
  • {"Each<T>"}: Testa se o tipo é da natureza de Array (o ArrayLike não obriga que seja um array, só que o mesmo tenha uma interface de iterável como Array). Se o mesmo for um array, ele irá iterar nos itens e fazer um {"Unwrap<T[K]>"}, onde K é o índice no Array
interface interface PromiseLike<T>PromiseLike<function (type parameter) T in PromiseLike<T>T> {
  PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>then<function (type parameter) R1 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R1 = function (type parameter) T in PromiseLike<T>T, function (type parameter) R2 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R2 = never>(
    resolve: ((value: T) => R1) | null | undefinedresolve?: ((value: Tvalue: function (type parameter) T in PromiseLike<T>T) => function (type parameter) R1 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R1) | undefined | null,
    reject: ((reason: any) => R2) | null | undefinedreject?: ((reason: anyreason: any) => function (type parameter) R2 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R2) | undefined | null
  ): interface PromiseLike<T>PromiseLike<function (type parameter) R1 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R1 | function (type parameter) R2 in PromiseLike<T>.then<R1 = T, R2 = never>(resolve?: ((value: T) => R1) | undefined | null, reject?: ((reason: any) => R2) | undefined | null): PromiseLike<R1 | R2>R2>;
}

type type Unwrap<T> = T extends Promise<infer U> ? U : T extends (...args: any) => Promise<infer U> ? U : T extends (...args: any) => infer U ? U : TUnwrap<function (type parameter) T in type Unwrap<T>T> = function (type parameter) T in type Unwrap<T>T extends interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<infer function (type parameter) UU>
? function (type parameter) UU : function (type parameter) T in type Unwrap<T>T extends (...args: anyargs: any) => interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<infer function (type parameter) UU>
? function (type parameter) UU : function (type parameter) T in type Unwrap<T>T extends (...args: anyargs: any) => infer function (type parameter) UU ? function (type parameter) UU : function (type parameter) T in type Unwrap<T>T; type type ReadonlyPromise<T> = Readonly<T> | Readonly<PromiseLike<T>>ReadonlyPromise<function (type parameter) T in type ReadonlyPromise<T>T> = type Readonly<T> = { readonly [P in keyof T]: T[P]; }
Make all properties in T readonly
Readonly
<function (type parameter) T in type ReadonlyPromise<T>T> | type Readonly<T> = { readonly [P in keyof T]: T[P]; }
Make all properties in T readonly
Readonly
<interface PromiseLike<T>PromiseLike<function (type parameter) T in type ReadonlyPromise<T>T>>;
type type Each<T> = T extends ArrayLike<any> ? { [K in keyof T]: Unwrap<T[K]>; } : TEach<function (type parameter) T in type Each<T>T> = function (type parameter) T in type Each<T>T extends interface ArrayLike<T>ArrayLike<any> ? { [function (type parameter) KK in keyof function (type parameter) T in type Each<T>T]: type Unwrap<T> = T extends Promise<infer U> ? U : T extends (...args: any) => Promise<infer U> ? U : T extends (...args: any) => infer U ? U : TUnwrap<function (type parameter) T in type Each<T>T[function (type parameter) KK]>; } : function (type parameter) T in type Each<T>T; const const PromiseAll: <T>(...values: ReadonlyPromise<T>[]) => Promise<Each<T>>PromiseAll = async <function (type parameter) T in <T>(...values: ReadonlyPromise<T>[]): Promise<Each<T>>T>(...values: ReadonlyPromise<T>[]values: type ReadonlyPromise<T> = Readonly<T> | Readonly<PromiseLike<T>>ReadonlyPromise<function (type parameter) T in <T>(...values: ReadonlyPromise<T>[]): Promise<Each<T>>T>[]): interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<type Each<T> = T extends ArrayLike<any> ? { [K in keyof T]: Unwrap<T[K]>; } : TEach<function (type parameter) T in <T>(...values: ReadonlyPromise<T>[]): Promise<Each<T>>T>> => var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise
.PromiseConstructor.all<any>(values: any): Promise<{ -readonly [P in string | number | symbol]: Awaited<any>; }> (+1 overload)
Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.
@paramvalues An array of Promises.@returnsA new Promise.
all
(values: ReadonlyPromise<T>[]values[0] as any) as any;
const const promise: Promise<string>promise = new
var Promise: PromiseConstructor
new <string>(executor: (resolve: (value: string | globalThis.PromiseLike<string>) => void, reject: (reason?: any) => void) => void) => Promise<string>
Creates a new Promise.
@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
<string>((res: (value: string | globalThis.PromiseLike<string>) => voidres) => res: (value: string | globalThis.PromiseLike<string>) => voidres("ok"));
// experimente mexer no array para verificar os tipos de promise sendo resolvidas const const a: readonly [Promise<string>, 1, Promise<string>, Promise<string>, Promise<string>, Promise<string>, Promise<string>, () => void]a = [const promise: Promise<string>promise, 1, const promise: Promise<string>promise, const promise: Promise<string>promise, const promise: Promise<string>promise, const promise: Promise<string>promise, const promise: Promise<string>promise, () => {}] as type const = readonly [Promise<string>, 1, Promise<string>, Promise<string>, Promise<string>, Promise<string>, Promise<string>, () => void]const; const PromiseAll: <[Promise<string>, Promise<string>, number]>(...values: ReadonlyPromise<[Promise<string>, Promise<string>, number]>[]) => Promise<Each<[...]>>PromiseAll([const promise: Promise<string>promise, const promise: Promise<string>promise, 1]).Promise<[string, string, number]>.then<void, never>(onfulfilled?: ((value: [string, string, number]) => void | globalThis.PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => never | globalThis.PromiseLike<never>) | null | undefined): Promise<...>
Attaches callbacks for the resolution and/or rejection of the Promise.
@paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
then
((e: [string, string, number]e) => {
const const f: stringf = e: [string, string, number]e[1]; // experimente trocar o índice para verificar os tipos var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(e: [string, string, number]e, const f: stringf);
});