Typescript 101 - [2]
Não sei criar tipos pra N objetos, e agora?
Introdução
Este artigo apresenta técnicas de tipagem mais complexas, com exemplos que exigem um entendimento mais aprofundado de TypeScript.
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).
Um exemplo simples para entender o conceito:
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 built-in do TypeScript que oferecem recursos adicionais para a criação de tipos. 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
O exemplo a seguir aplica generics em um contexto de Redux com React:
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 optionalPartial<{
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. A solução exigiu alguns contornos específicos para que a tipagem funcionasse corretamente.
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
PromiseAlldeverá ser passado comoas constpara garantir o readonly. - Foi usado
...values: ReadonlyPromise<T>[]evalues[0]foram usados para "trapacear" a tipagem original, assim como oas anynoPromise.alle após a invocação do método.
A seguir, uma explicação de cada tipo utilizado:
- {"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 operationPromise<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 operationPromise<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 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]>; } : 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 operationPromise<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: PromiseConstructorRepresents the completion of an asynchronous operationPromise.PromiseConstructor.all<any>(values: any): Promise<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>) => 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<...>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) => 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: 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);
});
Obrigado pelo seu tempo, tamo junto e até a próxima