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 Arrays<T> = T[];
const a: 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 ActionsTypes {
Open = "Action/Open",
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 Action<T = {}> = { type: ActionsTypes } & T;
const initialState = {
loading: false,
user: null,
authorized: false,
};
// O uso do nosso Action<T> fica transparente e facilita na tipagem das ações de nosso reducer
type AuthActions = Action<
Partial<{
login: string;
mock: null;
}>
>;
const authReducer = (state = initialState, action: AuthActions) => {
switch (action.type) {
case ActionsTypes.Close:
return { ...state, login: "" };
case ActionsTypes.Open:
return { ...state, login: action.login };
default:
return 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 PromiseLike<T> {
then<R1 = T, R2 = never>(
resolve?: ((value: T) => R1) | undefined | null,
reject?: ((reason: any) => R2) | undefined | null
): PromiseLike<R1 | R2>;
}
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;
type ReadonlyPromise<T> = Readonly<T> | Readonly<PromiseLike<T>>;
type Each<T> = T extends ArrayLike<any>
? {
[K in keyof T]: Unwrap<T[K]>;
}
: T;
const PromiseAll = async <T>(...values: ReadonlyPromise<T>[]): Promise<Each<T>> => Promise.all(values[0] as any) as any;
const promise = new Promise<string>((res) => res("ok"));
// experimente mexer no array para verificar os tipos de promise sendo resolvidas
const a = [promise, 1, promise, promise, promise, promise, promise, () => {}] as const;
PromiseAll([promise, promise, 1]).then((e) => {
const f = e[1]; // experimente trocar o índice para verificar os tipos
console.log(e, f);
});
Obrigado pelo seu tempo, tamo junto e até a próxima