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 ActionsTypes
ActionsTypes {
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: ActionsTypes
type: enum ActionsTypes
ActionsTypes } & function (type parameter) T in type Action<T = {}>
T;
const const initialState: {
loading: boolean;
user: null;
authorized: boolean;
}
initialState = {
loading: boolean
loading: false,
user: null
user: null,
authorized: boolean
authorized: 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 optionalPartial<{
login: string
login: string;
mock: null
mock: 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: AuthActions
action: type AuthActions = {
type: ActionsTypes;
} & Partial<{
login: string;
mock: null;
}>
AuthActions) => {
switch (action: AuthActions
action.type: ActionsTypes
type) {
case enum ActionsTypes
ActionsTypes.function (enum member) ActionsTypes.Close = "Action/Close"
Close:
return { ...state: {
loading: boolean;
user: null;
authorized: boolean;
}
state, login: string
login: "" };
case enum ActionsTypes
ActionsTypes.function (enum member) ActionsTypes.Open = "Action/Open"
Open:
return { ...state: {
loading: boolean;
user: null;
authorized: boolean;
}
state, login: string | undefined
login: action: AuthActions
action.login?: string | undefined
login };
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
- Cada item da minha promise deveria ser readonly para que os tipos pudessem ser tratados como constantes/imutáveis.
- O array passado para minha função
PromiseAll
deverá ser passado comoas const
para garantir o readonly. - Foi usado
...values: ReadonlyPromise<T>[]
evalues[0]
foram usados para "trapacear" a tipagem original, assim como oas any
noPromise.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 | undefined
resolve?: ((value: T
value: 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 | undefined
reject?: ((reason: any
reason: 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 : T
Unwrap<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 operationPromise<infer function (type parameter) U
U>
? function (type parameter) U
U
: function (type parameter) T in type Unwrap<T>
T extends (...args: any
args: any) => interface Promise<T>
Represents the completion of an asynchronous operationPromise<infer function (type parameter) U
U>
? function (type parameter) U
U
: function (type parameter) T in type Unwrap<T>
T extends (...args: any
args: any) => infer function (type parameter) U
U
? function (type parameter) U
U
: 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 readonlyReadonly<function (type parameter) T in type ReadonlyPromise<T>
T> | type Readonly<T> = { readonly [P in keyof T]: T[P]; }
Make all properties in T readonlyReadonly<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]>; } : T
Each<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) K
K 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 : T
Unwrap<function (type parameter) T in type Each<T>
T[function (type parameter) K
K]>;
}
: 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 operationPromise<type Each<T> = T extends ArrayLike<any> ? { [K in keyof T]: Unwrap<T[K]>; } : T
Each<function (type parameter) T in <T>(...values: ReadonlyPromise<T>[]): Promise<Each<T>>
T>> => var Promise: PromiseConstructor
Represents the completion of an asynchronous operationPromise.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.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.Promise<string>((res: (value: string | globalThis.PromiseLike<string>) => void
res) => res: (value: string | globalThis.PromiseLike<string>) => void
res("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.then((e: [string, string, number]
e) => {
const const f: string
f = e: [string, string, number]
e[1]; // experimente trocar o índice para verificar os tipos
var console: Console
console.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: string
f);
});