El autor del artículo que estamos traduciendo hoy dice que TypeScript es increíble. Cuando comenzó a usar TS, le gustó mucho la libertad inherente a este lenguaje. Cuanto más esfuerzo ponga un programador en su trabajo con mecanismos específicos de TS, mayores serán los beneficios que recibirá. Luego usó anotaciones de tipo solo periódicamente. Algunas veces utilizó las oportunidades para completar el código y las sugerencias del compilador, pero se basó principalmente en su propia visión de las tareas que resolvió.
Con el tiempo, el autor de este material se dio cuenta de que cada vez que pasa por alto los errores detectados en la etapa de compilación, coloca una bomba de tiempo en su código que puede explotar durante la ejecución del programa. Cada vez que "luchaba" con errores usando una construcción simple
as any
, tenía que pagar por ello con muchas horas de depuración.

Como resultado, llegó a la conclusión de que es mejor no hacerlo. Se hizo amigo del compilador, comenzó a prestar atención a sus pistas. El compilador encuentra problemas en el código y los informa mucho antes de que puedan causar un daño real. El autor del artículo, al verse a sí mismo como desarrollador, se dio cuenta de que el compilador es su mejor amigo, ya que lo protege de sí mismo. ¿Cómo no recordar las palabras de Albus Dumbledore: "Se necesita mucho coraje para hablar en contra de tus enemigos, pero no se requiere menos que eso para hablar en contra de tus amigos".
No importa cuán bueno sea el compilador, no siempre es fácil complacerlo. A veces, evitar el uso de
any
tipo es muy difícil. Y a veces parece que
any
es la única solución razonable para algún problema.
Este material se enfoca en dos situaciones. Al evitar el uso de
any
tipo en ellos, puede garantizar la seguridad de tipo del código, abrir las posibilidades para su reutilización y hacerlo intuitivo.
Genéricos
Supongamos que estamos trabajando en una base de datos de una escuela. Escribimos una función auxiliar muy conveniente
getBy
. Para obtener el objeto que representa al estudiante por su nombre, podemos usar un comando de la forma
getBy(model, "name", "Harry")
. Echemos un vistazo a la implementación de este mecanismo (aquí, para no complicar el código, la base de datos está representada por una matriz ordinaria).
type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) { return model.filter(item => item[prop] === value)[0] }
Como puede ver, tenemos una buena función, pero no utiliza anotaciones de tipo, y su ausencia también significa que dicha función no se puede llamar de tipo seguro. Arreglarlo
function getBy(model: Student[], prop: string, value): Student | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student
Entonces nuestra función ya se ve mucho mejor. El compilador ahora conoce el tipo de resultado esperado, esto será útil más adelante. Sin embargo, para lograr un trabajo seguro con los tipos, sacrificamos las posibilidades de reutilizar la función. ¿Qué pasa si alguna vez necesitamos usarlo para obtener otras entidades? No puede ser que esta función no pueda mejorarse de ninguna manera. Y realmente lo es.
En TypeScript, como en otros lenguajes fuertemente tipados, podemos usar genéricos, que también se denominan "tipos genéricos", "tipos universales", "generalizaciones".
Un genérico es similar a una variable regular, pero en lugar de algún valor, contiene una definición de tipo. Reescribimos el código de nuestra función para que, en lugar del tipo
Student
, use el tipo universal
T
function getBy<T>(model: T[], prop: string, value): T | null { return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student
Belleza! Ahora la función es ideal para la reutilización, mientras que la seguridad de tipo sigue estando de nuestro lado. Observe cómo el tipo de
Student
se establece explícitamente en la última línea del fragmento de código anterior donde
T
genérica. Esto se hace para que el ejemplo sea lo más claro posible, pero el compilador, de hecho, puede derivar independientemente el tipo necesario, por lo que en los siguientes ejemplos no haremos tales refinamientos de tipo.
Así que ahora tenemos una función auxiliar confiable adecuada para su reutilización. Sin embargo, todavía se puede mejorar. ¿Qué sucede si se comete un error al ingresar el segundo parámetro y en lugar de
"name"
parece haber
"naem"
? La función se comportará como si el alumno que busca simplemente no se encuentra en la base de datos y, lo que es más desagradable, no producirá ningún error. Esto puede provocar una depuración a largo plazo.
Para protegernos contra tales errores, presentamos otro tipo universal,
P
Además, es necesario que
P
sea una clave de tipo
T
, por lo tanto, si
Student
usa
Student
aquí, entonces es necesario que
P
sea la cadena
"name"
,
"age"
o
"hasScar"
. Aquí te explicamos cómo hacerlo.
function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'.
El uso de genéricos y la
keyof
es un truco muy poderoso. Si escribe programas en un IDE que admite TypeScript, al ingresar argumentos, puede aprovechar las capacidades de autocompletado, lo cual es muy conveniente.
Sin embargo, todavía no hemos terminado de trabajar en la función
getBy
. Ella tiene un tercer argumento, cuyo tipo aún no hemos establecido. Esto no nos conviene en absoluto. Hasta ahora, no podíamos saber de antemano de qué tipo debería ser, ya que depende de lo que pasemos como segundo argumento. Pero ahora, dado que tenemos el tipo
P
, podemos inferir dinámicamente el tipo para el tercer argumento. El tipo del tercer argumento será finalmente
T[P]
. Como resultado, si
T
es
Student
y
P
es
"age"
, entonces
T[P]
será de tipo
number
.
function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null { return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //
Espero que ahora tenga una comprensión absolutamente clara de cómo usar los genéricos en TypeScript, pero si desea experimentar muy bien con todo lo que desea experimentar con el código discutido aquí, puede echar un vistazo
aquí .
Extendiendo tipos existentes
A veces podemos encontrar la necesidad de agregar datos o funcionalidades a interfaces cuyo código no podemos cambiar. Es posible que deba cambiar el objeto estándar, por ejemplo, agregar alguna propiedad al objeto de
window
o extender el comportamiento de alguna biblioteca externa como
Express
. Y en ambos casos, no tiene la capacidad de afectar directamente el objeto con el que desea trabajar.
getBy
una solución a este problema agregando la función
getBy
que ya conoce al prototipo de
Array
. Esto nos permitirá, utilizando esta función, construir construcciones sintácticas más precisas. Por el momento, no estamos hablando de si es bueno o malo expandir objetos estándar, ya que nuestro objetivo principal es estudiar el enfoque en consideración.
Si intentamos agregar una función al prototipo de
Array
, al compilador no le gustará mucho esto:
Array.prototype.getBy = function <T, P extends keyof T>( this: T[], prop: P, value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") // ... ?
Si tratamos de tranquilizar al compilador utilizando periódicamente la construcción
as any
construcción, anularemos todo lo que hemos logrado. El compilador permanecerá en silencio, pero puede olvidarse del trabajo seguro con tipos.
Sería mejor extender el tipo de
Array
, pero antes de hacerlo, hablemos sobre cómo TypeScript maneja las situaciones cuando dos interfaces del mismo tipo están presentes en el código. Aquí se aplica un esquema simple de acción. Los anuncios, si es posible, se combinarán. Si no puede combinarlos, el sistema generará un error.
Entonces este código funciona:
interface Wand { length: number } interface Wand { core: string } const myWand: Wand = { length: 11, core: "phoenix feather" }
Y este no es:
interface Wand { length: number } interface Wand { length: string }
Ahora, habiendo lidiado con esto, vemos que nos enfrentamos a una tarea bastante simple. Es decir, todo lo que tenemos que hacer es declarar la interfaz
Array<T>
y agregarle la función
getBy
.
interface Array<T> { getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>( this: T[], prop: P, value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); // ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //
Tenga en cuenta que la mayor parte del código que probablemente escriba en los archivos del módulo, por lo tanto, para realizar cambios en la interfaz de la
Array
, necesitará acceso al alcance global. Puede hacer esto colocando la definición de tipo dentro de
declare global
. Por ejemplo, así:
declare global { interface Array<T> { getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } }
Si va a expandir la interfaz de una biblioteca externa, lo más probable es que necesite acceso al
namespace
esta biblioteca. Aquí hay un ejemplo de cómo agregar el campo
userId
a
Request
de la biblioteca
Express
:
declare global { namespace Express { interface Request { userId: string; } } }
Puede experimentar con el código en esta sección
aquí .
Resumen
En este artículo, analizamos técnicas para usar genéricos y extensiones de tipo en TypeScript. Esperamos que lo que aprendió hoy lo ayude a escribir un código confiable, comprensible y de tipo seguro.
Estimados lectores! ¿Cómo te sientes acerca de cualquier tipo en TypeScript?
