Escritura estática dinámica en TypeScript
Publicado: 2022-03-10JavaScript es un lenguaje de programación inherentemente dinámico. Nosotros, como desarrolladores, podemos expresar mucho con poco esfuerzo, y el lenguaje y su tiempo de ejecución determinan lo que pretendíamos hacer. ¡Esto es lo que hace que JavaScript sea tan popular entre los principiantes y lo que hace que los desarrolladores experimentados sean productivos! Sin embargo, hay una advertencia: ¡debemos estar alerta! Errores, errores tipográficos, comportamiento correcto del programa: ¡Mucho de eso sucede en nuestras cabezas!
Echa un vistazo al siguiente ejemplo.
app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { res.status(20).send({ message: "Got you, user " + req.params.userId }); } })
Tenemos un servidor de estilo https://expressjs.com/ que nos permite definir una ruta (o ruta) y ejecuta una devolución de llamada si se solicita la URL.
La devolución de llamada toma dos argumentos:
- El objeto de la
request
.
Aquí obtenemos información sobre el método HTTP utilizado (p. ej., GET, POST, PUT, DELETE) y parámetros adicionales que entran. En este ejemplo, el ID de usuario debeuserID
a un parámetrouserID
de usuario que, bueno, contiene el ID del usuario. - La
response
o el objeto dereply
.
Aquí queremos preparar una respuesta adecuada del servidor al cliente. Queremos enviar los códigos de estado correctos (status
del método) y enviar la salida JSON por cable.
Lo que vemos en este ejemplo está muy simplificado, pero da una buena idea de lo que estamos haciendo. ¡El ejemplo anterior también está plagado de errores! Echar un vistazo:
app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { /* Error 1 */ res.status(20).send({ /* Error 2 */ message: "Welcome, user " + req.params.userId /* Error 3 */ }); } })
¡Oh wow! ¿Tres líneas de código de implementación y tres errores? ¿Lo que ha sucedido?
- El primer error tiene matices. Si bien le decimos a nuestra aplicación que queremos escuchar las solicitudes GET (por lo tanto,
app.get
), solo hacemos algo si el método de solicitud es POST . En este punto particular de nuestra aplicación,req.method
no puede ser POST . Por lo tanto, nunca enviaríamos ninguna respuesta, lo que podría provocar tiempos de espera inesperados. - ¡Genial que enviemos explícitamente un código de estado! Sin embargo,
20
no es un código de estado válido. Es posible que los clientes no entiendan lo que está sucediendo aquí. - Esta es la respuesta que queremos enviar de vuelta. Accedemos a los argumentos analizados pero tenemos un error tipográfico medio. Es
userID
de usuario, nouserId
de usuario. Todos nuestros usuarios serían recibidos con “¡Bienvenido, usuario indefinido!”. ¡Algo que definitivamente has visto en la naturaleza!
¡Y cosas así pasan! Especialmente en JavaScript. Ganamos en expresividad, ni una sola vez tuvimos que preocuparnos por los tipos, sino que debemos prestar mucha atención a lo que estamos haciendo.
Aquí también es donde JavaScript recibe muchas críticas de los programadores que no están acostumbrados a los lenguajes de programación dinámicos. Por lo general, tienen compiladores que les señalan posibles problemas y detectan errores por adelantado. Pueden parecer presumidos cuando fruncen el ceño ante la cantidad de trabajo adicional que tienes que hacer en tu cabeza para asegurarte de que todo funcione bien. Incluso podrían decirle que JavaScript no tiene tipos. Lo cual no es cierto.
Anders Hejlsberg, el arquitecto principal de TypeScript, dijo en su discurso de apertura de MS Build 2017 que “ no es que JavaScript no tenga un sistema de tipos. Simplemente no hay manera de formalizarlo ”.
Y este es el propósito principal de TypeScript. TypeScript quiere comprender su código JavaScript mejor que usted. Y cuando TypeScript no pueda descifrar lo que quiere decir, puede ayudar brindando información de tipo adicional.
Escritura básica
Y esto es lo que vamos a hacer ahora. Tomemos el método get
de nuestro servidor de estilo Express y agreguemos suficiente información de tipo para que podamos excluir tantas categorías de errores como sea posible.
Comenzamos con alguna información de tipo básico. Tenemos un objeto de app
que apunta a una función de get
. La función get
toma path
, que es una cadena, y una devolución de llamada.
const app = { get, /* post, put, delete, ... to come! */ }; function get(path: string, callback: CallbackFn) { // to be implemented --> not important right now }
Mientras que string
es un tipo básico, llamado primitivo , CallbackFn
es un tipo compuesto que tenemos que definir explícitamente.
CallbackFn
es un tipo de función que toma dos argumentos:
-
req
, que es de tipoServerRequest
-
reply
que es de tipoServerReply
CallbackFn
devuelve void
.
type CallbackFn = (req: ServerRequest, reply: ServerReply) => void;
ServerRequest
es un objeto bastante complejo en la mayoría de los marcos. Hacemos una versión simplificada con fines de demostración. Pasamos una cadena de method
, para "GET"
, "POST"
, "PUT"
, "DELETE"
, etc. También tiene un registro de params
. Los registros son objetos que asocian un conjunto de claves con un conjunto de propiedades. Por ahora, queremos permitir que cada clave de string
se asigne a una propiedad de string
. Refactorizaremos este más tarde.
type ServerRequest = { method: string; params: Record<string, string>; };
Para ServerReply
, presentamos algunas funciones, sabiendo que un objeto ServerReply
real tiene mucho más. Una función de send
toma un argumento opcional con los datos que queremos enviar. Y tenemos la posibilidad de establecer un código de estado con la función de status
.
type ServerReply = { send: (obj?: any) => void; status: (statusCode: number) => ServerReply; };
Eso ya es algo, y podemos descartar un par de errores:
app.get("/api/users/:userID", function(req, res) { if(req.method === 2) { // ^^^^^^^^^^^^^^^^^ Error, type number is not assignable to string res.status("200").send() // ^^^^^ Error, type string is not assignable to number } })
Pero aún podemos enviar códigos de estado incorrectos (cualquier número es posible) y no tenemos idea de los posibles métodos HTTP (cualquier cadena es posible). Vamos a refinar nuestros tipos.
Conjuntos más pequeños
Puede ver los tipos primitivos como un conjunto de todos los valores posibles de esa categoría determinada. Por ejemplo, string
incluye todas las cadenas posibles que se pueden expresar en JavaScript, number
incluye todos los números posibles con precisión de doble flotante. boolean
incluye todos los valores booleanos posibles, que son true
y false
.
TypeScript le permite refinar esos conjuntos a subconjuntos más pequeños. Por ejemplo, podemos crear un Method
de tipo que incluya todas las cadenas posibles que podemos recibir para los métodos HTTP:
type Methods= "GET" | "POST" | "PUT" | "DELETE"; type ServerRequest = { method: Methods; params: Record<string, string>; };
El Method
es un conjunto más pequeño del conjunto de string
más grande. Method
es un tipo de unión de tipos literales. Un tipo literal es la unidad más pequeña de un conjunto dado. Una cadena literal. Un número literal. No hay ambigüedad. Es simplemente "GET"
. Los pones en una unión con otros tipos literales, creando un subconjunto de cualquier tipo más grande que tengas. También puede hacer un subconjunto con tipos literales de string
y number
, o diferentes tipos de objetos compuestos. Hay muchas posibilidades para combinar y poner tipos literales en uniones.
Esto tiene un efecto inmediato en la devolución de llamada de nuestro servidor. De repente, podemos diferenciar entre esos cuatro métodos (o más si es necesario), y podemos agotar todas las posibilidades en el código. TypeScript nos guiará:
app.get("/api/users/:userID", function (req, res) { // at this point, TypeScript knows that req.method // can take one of four possible values switch (req.method) { case "GET": break; case "POST": break; case "DELETE": break; case "PUT": break; default: // here, req.method is never req.method; } });
Con cada declaración de case
que haga, TypeScript puede brindarle información sobre las opciones disponibles. Pruébelo usted mismo. Si agotó todas las opciones, TypeScript le dirá en su rama default
que esto never
puede suceder. Este es literalmente el tipo never
, lo que significa que posiblemente haya llegado a un estado de error que debe manejar.
Esa es una categoría de errores menos. Ahora sabemos exactamente qué posibles métodos HTTP están disponibles.
Podemos hacer lo mismo con los códigos de estado HTTP, definiendo un subconjunto de números válidos que puede tomar statusCode
:
type StatusCode = 100 | 101 | 102 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 444 | 449 | 450 | 451 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 598 | 599; type ServerReply = { send: (obj?: any) => void; status: (statusCode: StatusCode) => ServerReply; };
Type StatusCode
es nuevamente un tipo de unión. Y con eso, excluimos otra categoría de errores. De repente, un código como ese falla:
app.get("/api/user/:userID", (req, res) => { if(req.method === "POS") { // ^^^^^^^^^^^^^^^^^^^ 'Methods' and '"POS"' have no overlap. res.status(20) // ^^ '20' is not assignable to parameter of type 'StatusCode' } })
¡Y nuestro software se vuelve mucho más seguro! ¡Pero podemos hacer más!Introducir genéricos
Cuando definimos una ruta con app.get
, implícitamente sabemos que el único método HTTP posible es "GET"
. Pero con nuestras definiciones de tipo, todavía tenemos que verificar todas las partes posibles de la unión.
El tipo de CallbackFn
es correcto, ya que podríamos definir funciones de devolución de llamada para todos los métodos HTTP posibles, pero si llamamos explícitamente a app.get
, sería bueno ahorrar algunos pasos adicionales que solo son necesarios para cumplir con los tipos.
¡Los genéricos de TypeScript pueden ayudar! Los genéricos son una de las características principales de TypeScript que le permiten obtener el comportamiento más dinámico de los tipos estáticos. En TypeScript en 50 lecciones, dedicamos los últimos tres capítulos a profundizar en todas las complejidades de los genéricos y su funcionalidad única.
Lo que necesita saber en este momento es que queremos definir ServerRequest
de manera que podamos especificar una parte de los Methods
en lugar del conjunto completo. Para eso, usamos la sintaxis genérica donde podemos definir parámetros como lo haríamos con las funciones:
type ServerRequest<Met extends Methods> = { method: Met; params: Record<string, string>; };
Esto es lo que pasa:
-
ServerRequest
se convierte en un tipo genérico, como lo indican los corchetes angulares - Definimos un parámetro genérico llamado
Met
, que es un subconjunto deMethods
de tipo - Usamos este parámetro genérico como una variable genérica para definir el método.
También le animo a que consulte mi artículo sobre la denominación de parámetros genéricos.
Con ese cambio, podemos especificar diferentes ServerRequest
sin duplicar cosas:
type OnlyGET = ServerRequest<"GET">; type OnlyPOST = ServerRequest<"POST">; type POSTorPUT = ServerRquest<"POST" | "PUT">;
Como cambiamos la interfaz de ServerRequest
, tenemos que hacer cambios en todos nuestros otros tipos que usan ServerRequest
, como CallbackFn
y la función get
:
type CallbackFn<Met extends Methods> = ( req: ServerRequest<Met>, reply: ServerReply ) => void; function get(path: string, callback: CallbackFn<"GET">) { // to be implemented }
Con la función get
, pasamos un argumento real a nuestro tipo genérico. Sabemos que esto no será solo un subconjunto de Methods
, sabemos exactamente con qué subconjunto estamos tratando.
Ahora, cuando usamos app.get
, solo tenemos un valor posible para req.method
:
app.get("/api/users/:userID", function (req, res) { req.method; // can only be get });
Esto garantiza que no asumamos que los métodos HTTP como "POST"
o similares están disponibles cuando creamos una devolución de llamada app.get
. Sabemos exactamente a lo que nos enfrentamos en este punto, así que reflejémoslo en nuestros tipos.
Ya hicimos mucho para asegurarnos de que request.method
se escriba razonablemente y represente el estado real de las cosas. Un buen beneficio que obtenemos al crear un subconjunto del tipo de unión Methods
es que podemos crear una función de devolución de llamada de propósito general fuera de app.get
que sea de tipo seguro:
const handler: CallbackFn<"PUT" | "POST"> = function(res, req) { res.method // can be "POST" or "PUT" }; const handlerForAllMethods: CallbackFn<Methods> = function(res, req) { res.method // can be all methods }; app.get("/api", handler); // ^^^^^^^ Nope, we don't handle "GET" app.get("/api", handlerForAllMethods); // This works
Parámetros de escritura
Lo que aún no hemos tocado es escribir el objeto params
. Hasta ahora, tenemos un registro que permite acceder a cada clave string
. ¡Es nuestra tarea ahora hacer eso un poco más específico!
Hacemos eso agregando otra variable genérica. Uno para los métodos, otro para las posibles claves en nuestro Record
:
type ServerRequest<Met extends Methods, Par extends string = string> = { method: Met; params: Record<Par, string>; };
La variable de tipo genérico Par
puede ser un subconjunto del tipo string
y el valor predeterminado es cada cadena. Con eso, podemos decirle a ServerRequest
qué claves esperamos:
// request.method = "GET" // request.params = { // userID: string // } type WithUserID = ServerRequest<"GET", "userID">
Agreguemos el nuevo argumento a nuestra función get
y el tipo CallbackFn
, para que podamos establecer los parámetros solicitados:
function get<Par extends string = string>( path: string, callback: CallbackFn<"GET", Par> ) { // to be implemented } type CallbackFn<Met extends Methods, Par extends string> = ( req: ServerRequest<Met, Par>, reply: ServerReply ) => void;
Si no establecemos Par
explícitamente, el tipo funciona como estamos acostumbrados, ya que Par
está predeterminado en string
. Sin embargo, si lo configuramos, de repente tenemos una definición adecuada para el objeto req.params
.
app.get<"userID">("/api/users/:userID", function (req, res) { req.params.userID; // Works!! req.params.anythingElse; // doesn't work!! });
¡Eso es genial! Sin embargo, hay una pequeña cosa que se puede mejorar. Todavía podemos pasar cada cadena al argumento de path
de app.get
. ¿No sería mejor si pudiéramos reflejar a Par
allí también?
¡Podemos! Con el lanzamiento de la versión 4.1, TypeScript puede crear tipos de literales de plantilla . Sintácticamente, funcionan como literales de plantilla de cadena, pero en un nivel de tipo. Donde pudimos dividir la string
establecida en subconjuntos con tipos de literales de cadena (como hicimos con Métodos), los tipos de literales de plantilla nos permiten incluir un espectro completo de cadenas.
Vamos a crear un tipo llamado IncludesRouteParams
, donde queremos asegurarnos de que Par
esté correctamente incluido en la forma Express de agregar dos puntos delante del nombre del parámetro:
type IncludesRouteParams<Par extends string> = | `${string}/:${Par}` | `${string}/:${Par}/${string}`;
El tipo genérico IncludesRouteParams
toma un argumento, que es un subconjunto de string
. Crea un tipo de unión de dos literales de plantilla:
- El primer literal de la plantilla comienza con cualquier
string
, luego incluye un carácter/
seguido de un carácter:
, seguido del nombre del parámetro. Esto asegura que capturamos todos los casos en los que el parámetro está al final de la cadena de ruta. - El segundo literal de la plantilla comienza con cualquier
string
, seguido del mismo patrón de/
:
y el nombre del parámetro. Luego tenemos otro carácter/
, seguido de cualquier cadena. Esta rama del tipo de unión se asegura de que atrapemos todos los casos en los que el parámetro está en algún lugar dentro de una ruta.
Así es como se comporta el nombre de parámetro " IncludesRouteParams
" con userID
de usuario en diferentes casos de prueba:
const a: IncludeRouteParams<"userID"> = "/api/user/:userID" // const a: IncludeRouteParams<"userID"> = "/api/user/:userID/orders" // const a: IncludeRouteParams<"userID"> = "/api/user/:userId" // const a: IncludeRouteParams<"userID"> = "/api/user" // const a: IncludeRouteParams<"userID"> = "/api/user/:userIDAndmore" //
Incluyamos nuestro nuevo tipo de utilidad en la declaración de la función get
.
function get<Par extends string = string>( path: IncludesRouteParams<Par>, callback: CallbackFn<"GET", Par> ) { // to be implemented } app.get<"userID">( "/api/users/:userID", function (req, res) { req.params.userID; // YEAH! } );
¡Genial! ¡Obtenemos otro mecanismo de seguridad para asegurarnos de no perder la oportunidad de agregar los parámetros a la ruta real! Que poderoso
Encuadernaciones genéricas
Pero adivina qué, todavía no estoy contento con eso. Hay algunos problemas con ese enfoque que se vuelven evidentes en el momento en que sus rutas se vuelven un poco más complejas.
- El primer problema que tengo es que necesitamos indicar explícitamente nuestros parámetros en el parámetro de tipo genérico. Tenemos que vincular
Par
a"userID"
, aunque lo especificaríamos de todos modos en el argumento de ruta de la función. ¡Esto no es JavaScript-y! - Este enfoque solo maneja un parámetro de ruta. En el momento en que agregamos una unión, por ejemplo,
"userID" | "orderId"
"userID" | "orderId"
la verificación a prueba de fallas se cumple con solo uno de esos argumentos disponibles. Así es como funcionan los conjuntos. Puede ser uno, o el otro.
Tiene que haber una mejor manera. Y ahí está. De lo contrario, este artículo terminaría con una nota muy amarga.
¡Invirtamos el orden! No intentemos definir los parámetros de ruta en una variable de tipo genérico, sino extraer las variables de la path
que pasamos como el primer argumento de app.get
.
Para llegar al valor real, tenemos que ver cómo funciona el enlace genérico en TypeScript. Tomemos esta función de identity
por ejemplo:
function identity<T>(inp: T) : T { return inp }
Puede que sea la función genérica más aburrida que jamás hayas visto, pero ilustra perfectamente un punto. La identity
toma un argumento y devuelve la misma entrada nuevamente. El tipo es el tipo genérico T
y también devuelve el mismo tipo.
Ahora podemos vincular T
a string
, por ejemplo:
const z = identity<string>("yes"); // z is of type string
Esta vinculación explícitamente genérica garantiza que solo pasemos strings
a la identity
y, dado que vinculamos explícitamente, el tipo de retorno también es una string
. Si nos olvidamos de enlazar, sucede algo interesante:
const y = identity("yes") // y is of type "yes"
En ese caso, TypeScript infiere el tipo del argumento que pasa y vincula T
al tipo de cadena literal "yes"
. Esta es una excelente manera de convertir un argumento de función en un tipo literal, que luego usamos en nuestros otros tipos genéricos.
Hagámoslo adaptando app.get
.
function get<Path extends string = string>( path: Path, callback: CallbackFn<"GET", ParseRouteParams<Path>> ) { // to be implemented }
Eliminamos el tipo genérico Par
y añadimos Path
. Path
puede ser un subconjunto de cualquier string
. Establecemos la path
a este tipo genérico Path
, lo que significa que en el momento en que pasamos un parámetro para get
, capturamos su tipo literal de cadena. Pasamos Path
a un nuevo tipo genérico ParseRouteParams
que aún no hemos creado.
Trabajemos en ParseRouteParams
. Aquí, volvemos a cambiar el orden de los eventos. En lugar de pasar los parámetros de ruta solicitados al genérico para asegurarnos de que la ruta esté bien, pasamos la ruta de ruta y extraemos los posibles parámetros de ruta. Para eso, necesitamos crear un tipo condicional.
Tipos condicionales y tipos literales de plantillas recursivas
Los tipos condicionales son sintácticamente similares al operador ternario en JavaScript. Verifica una condición y, si se cumple la condición, devuelve la rama A; de lo contrario, devuelve la rama B. Por ejemplo:
type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}` ? P : never;
Aquí, verificamos si Rte
es un subconjunto de cada ruta que termina con el parámetro al final Express-style (con un "/:"
precedente). Si es así, inferimos esta cadena. Lo que significa que capturamos su contenido en una nueva variable. Si la condición se cumple, devolvemos la cadena recién extraída, de lo contrario, devolvemos nunca, como en: "No hay parámetros de ruta",
Si lo probamos, obtenemos algo como esto:
type Params = ParseRouteParams<"/api/user/:userID"> // Params is "userID" type NoParams = ParseRouteParams<"/api/user"> // NoParams is never --> no params!
Genial, eso ya es mucho mejor que lo que hicimos antes. Ahora, queremos capturar todos los demás parámetros posibles. Para eso, tenemos que añadir otra condición:
type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}/${infer Rest}` ? P | ParseRouteParams<`/${Rest}`> : Rte extends `${string}/:${infer P}` ? P : never;
Nuestro tipo condicional funciona ahora de la siguiente manera:
- En la primera condición, verificamos si hay un parámetro de ruta en algún lugar entre la ruta. Si es así, extraemos tanto el parámetro de ruta como todo lo que viene después. Devolvemos el parámetro de ruta
P
recién encontrado en una unión donde llamamos al mismo tipo genérico recursivamente conRest
. Por ejemplo, si pasamos la ruta"/api/users/:userID/orders/:orderID"
aParseRouteParams
, inferimos"userID"
enP
y"orders/:orderID"
enRest
. Llamamos al mismo tipo conRest
- Aquí es donde entra la segunda condición. Aquí comprobamos si hay un tipo al final. Este es el caso de
"orders/:orderID"
."orderID"
y devolvemos este tipo literal. - Si no queda más parámetro de ruta, regresamos nunca.
Dan Vanderkam muestra un tipo similar y más elaborado para ParseRouteParams
, pero el que ves arriba también debería funcionar. Si probamos nuestro ParseRouteParams
recién adaptado, obtenemos algo como esto:
// Params is "userID" type Params = ParseRouteParams<"/api/user/:userID"> // MoreParams is "userID" | "orderID" type MoreParams = ParseRouteParams<"/api/user/:userID/orders/:orderId">
Apliquemos este nuevo tipo y veamos cómo se ve nuestro uso final de app.get
.
app.get("/api/users/:userID/orders/:orderID", function (req, res) { req.params.userID; // YES!! req.params.orderID; // Also YES!!! });
Guau. ¡Eso se parece al código JavaScript que teníamos al principio!
Tipos estáticos para comportamiento dinámico
Los tipos que acabamos de crear para una función app.get
se aseguran de que excluyamos una tonelada de posibles errores:
- Solo podemos pasar códigos de estado numéricos adecuados a
res.status()
-
req.method
es una de las cuatro cadenas posibles, y cuando usamosapp.get
, sabemos que solo es"GET"
- Podemos analizar los parámetros de ruta y asegurarnos de que no tengamos errores tipográficos dentro de nuestra devolución de llamada.
Si observamos el ejemplo del principio de este artículo, obtenemos los siguientes mensajes de error:
app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { // ^^^^^^^^^^^^^^^^^^^^^ // This condition will always return 'false' // since the types '"GET"' and '"POST"' have no overlap. res.status(20).send({ // ^^ // Argument of type '20' is not assignable to // parameter of type 'StatusCode' message: "Welcome, user " + req.params.userId // ^^^^^^ // Property 'userId' does not exist on type // '{ userID: string; }'. Did you mean 'userID'? }); } })
¡Y todo eso antes de ejecutar nuestro código! Los servidores de estilo Express son un ejemplo perfecto de la naturaleza dinámica de JavaScript. Dependiendo del método que llame, la cadena que pase como primer argumento, muchos cambios de comportamiento dentro de la devolución de llamada. Tome otro ejemplo y todos sus tipos se ven completamente diferentes.
Pero con algunos tipos bien definidos, podemos detectar este comportamiento dinámico mientras editamos nuestro código. ¡En tiempo de compilación con tipos estáticos, no en tiempo de ejecución cuando las cosas se disparan!
Y este es el poder de TypeScript. Un sistema de tipo estático que intenta formalizar todo el comportamiento dinámico de JavaScript que todos conocemos tan bien. Si desea probar el ejemplo que acabamos de crear, diríjase al área de juegos de TypeScript y juegue con él.
En este artículo, tocamos muchos conceptos. Si desea obtener más información, consulte TypeScript en 50 lecciones, donde obtiene una introducción suave al sistema de tipos en lecciones pequeñas y fáciles de digerir. Las versiones de libros electrónicos están disponibles de inmediato, y el libro impreso será una gran referencia para su biblioteca de codificación.