No necesitas generics realmente, si no estás reusando código.
Pero, si no volver a escribir lo ya escrito es lo tuyo, las generics te van a permitir definir funciones tipadas sin estar sujetas a un solo un tipo. 🤔 Las funciones genéricas son capaces de recibir tipos como parámetros.
Las «generics» han estado aquí en lenguajes como C# y Java. Con la capacidad de crear componentes que pueden trabajar con una amplia variedad de tipos en vez de solo uno. Permitiendo que el usuario del componente use sus propios tipos.
Ahora que tenemos esta habilidad en TypeScript, lo mejor sería echarle un ojo, ¿no crees?
Por eso vamos a entenderlas con 2 ejemplos simples:
Supongamos que tenemos una función que agrega una llave a un objeto.
export const addNewKey = (key,value, obj)=>{ return {...obj, [key]:value} }
Sin «generics» tendríamos que agregar sus tipos, tal vez así:
export const addNewKey = ( key: number | string, value: any, obj: Record<string, any> ): Record<string, any> => { return { ...obj, [key]: value }; };
Y estaríamos atados para siempre a esos únicos tipos, a menos que usáramos any
para todo:
export const addNewKey = (key: any, value: any, obj: any): any => { return { ...obj, [key]: value }; };
Pero nos perderíamos toda la magia de TS y sus sugerencias.
Cuando empleamos any
se pierde la información sobre el tipo cuando la función termina. Si pasamos un número, la única información que tendremos de él será «Cualquier tipo puede ser devuelto»
y esto es completamente inútil. 💔
Mejor usemos «generics»:
export const addNewKey = <Key, Value, Obj>( key: Key, value: Value, obj: Obj ): Obj => { return { ...obj, [key]: value }; };
Observa las definiciones que agregamos Key
, Value
y Obj
dentro de esos «angle brackets» (<>).
Nosotros no decidimos qué tipo tendrán key
, value
y obj
lo va a decidir quien invoque a la función:
// El tipo del objecto type UserType = { email: string; [x: string | number]: string | number | boolean; }; // El objeto const user: UserType = { email: "blissmo@gmail.com", }; // Invocación pasando tipos addNewKey<string, string, UserType>("name", "blissmo", user);
Al invocar la función addNewKey
estamos pasando los argumentos necesarios, pero también incluimos el tipo de estos argumentos: <string, string, UserType>
. 🤯
A una función definida de esa forma, le llamamos generic function
, ya que puede trabajar con una multitud de tipos.
A diferencia de trabajar conany
, ahora no perdemos información en ningún punto de las invocaciones y retornos. ✅
Existe una segunda forma de invocar una función genérica. También podemos hacer uso de la “inferencia” (que si no sabes qué es inferencia checa esta entrada):
addNewKey("name", "blissmo", user);
Así es, el compilador de TS puede inferir los tipos a partir de los argumentos y colocar cada uno en las definiciones genéricas.
Esto puede ser útil con tipos sencillos, pero también puede fallar para user
en este caso o para otro tipo de estructura de datos. Es mejor definirlos (lo que hicimos primero).
Esta función recibe un código de Google que puede intercambiar por un access_token
Analicemos juntos todo lo que está pasando aquí:
/** * Este tipo es la forma de la respuesta de la función. * Puede tener una de estas dos formas. */ type Result = | { ok: boolean; access_token: string; } | { ok: boolean; error: Error }; /** * Este tipo es la forma de la respuesta de google * (solo el dato que se usa) */ type Data = { access_token: string; }; /** * Este tipo es la forma de nuestro objeto de llaves */ type EnvObject = { GOOGLE_SECRET: string; GOOGLE_CLIENT_ID: string; ENV: "production" | "development"; }; /** * Definimos como genericos a Code que extiende o se espera sea de * tipo string, esta extensión me es necesaria porque * code se usará dentro de new URLSearchParams({code}) * el tipo que esta función espera es string. */ export const getAccessToken = <Code extends string, Env extends EnvObject>( code: Code, env: Env /** * Devolvemos una promesa con nuestro tipo Result. */ ): Promise<Result> => { if (!env || !env.GOOGLE_SECRET || !env.GOOGLE_CLIENT_ID) throw new Error("missing env object"); const url = "https://oauth2.googleapis.com/token?" + new URLSearchParams({ code, client_secret: env.GOOGLE_SECRET, grant_type: "authorization_code", client_id: env.GOOGLE_CLIENT_ID, redirect_uri: env.ENV === "production" ? "www.production.com" : "http://localhost:8787", scope: "https://www.googleapis.com/auth/userinfo.email", }); return ( fetch(url, { method: "post", headers: { "content-type": "application/json" }, }) .then((r) => r.json()) /** * Esta es la devolución positiva, usamos nuestro tipo Data. */ .then((data) => ({ access_token: (data as Data).access_token, ok: true, })) /** * Esta es la devolución negativa, con la forma que definímos antes. */ .catch((e) => ({ error: e, ok: false })) ); }; /** * Podemos invocar nuestra función de la siguiente manera: * (En otro archivo) */ const env: EnvObject = { GOOGLE_SECRET: process.env.GOOGLE_SECRET, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, ENV: "development", }; getAccessToken<string, EnvObject>("asd3o2", env);
Seguro encontrarás mejores usos que yo, pero ¡oye! Ahora sabes usar generics.
Si este artículo te fue útil, por favor considera suscribirte. 🤓🍕📚🎧🌀👨🏻💻
BONUS: Puedes extender tus generics al definirlos, desde tipos presentes en el scope:
<Code extends number, User extends UserType>(code:Code,user:User):User
Abrazo. Bliss.
© 2016 - 2023 Fixtergeek