Saltar al contenido

Tech Blog | Domando los datos dinámicos en TypeScript

A mí realmente me gustan los tipos estáticos. Mucho. Cuando de otra manera estaría usando JavaScript, ahora he abrazado completamente el TypeScript.

Utilizar plenamente los tipos estáticos, con toda la seguridad que proporcionan, puede ser un poco complicado cuando se trata de datos dinámicos – como JSON de una llamada a la API.Este problema no es exclusivo de TypeScript, pero TypeScript tiene algunas consideraciones bastante singulares.

Tech Blog | Domando los datos dinámicos en TypeScript
Tech Blog | Domando los datos dinámicos en TypeScript

Consideremos algunos JSON que queremos analizar:

constrawJson=`{ "nombre": "Alice", "edad": 31}`constparsed=JSON.parse(rawJson)//$0027parsed$0027 es tipo $0027any$0027

Así es como lo haríamos también en JavaScript. De hecho, si pasas el cursor por encima del análisis en tu IDE, verás que su tipo es cualquiera. Esto significa que TypeScript nos permitirá hacer cualquier cosa con el análisis sin darnos ninguna comprobación de tipo estático:

console.log(parsed.nam)//imprime $0027undefined$0027

TypeScript no capta el error; felizmente se imprimirá sin definir.

Evitar cualquier

Para sacar el máximo provecho de TypeScript, deberías evitar usar cualquiera siempre que sea posible.1 Es difícil confiar en tus tipos estáticos cuando tienes lugares en tu código que evitan el sistema de tipos a través de any.In casos en los que realmente no conoces el tipo (como después de analizar algún JSON en bruto), usa unknown, una contrapartida segura de cualquier tipo.

Por ejemplo, podríamos definir una función de análisis más segura como esta:

constparseJson=(str: string):unknown=;JSON.parse(str)

El cuerpo es sólo un paso, pero la anotación de tipo de retorno desconocido hace que el tipo sea mucho más estrecho. Ahora, si tratamos de acceder a cualquier propiedad de nuestro JSON analizado, obtendremos un error de tipo:

constparsed=parseJson(rawJson)console.log(parsed.nam)//tipo de error: El objeto es del tipo $0027unknown$0027.console.log(parsed.name)//también un error de tipo - volveremos a este :)

Súper seguro, pero aún no muy útil. Está bien – nos obligará a ser explícitos sobre el tipo, como veremos más adelante.

Afirmaciones de tipo

Primero, definamos un tipo que coincida con el JSON:

typeUser={nombre: stringage: número}

Con esto, ahora podemos usar una afirmación de tipo de usuario en el valor analizado para obtener nuestra escritura estática:

constparsed=parseJson(rawJson)asUserconsole.log(parsed.nam)//tipo de error: La propiedad $0027nam$0027 no existe en typeconsole.log(parsed.name)//works

Esto efectivamente le dice a TypeScript, “Sé algo que tú no sabes; confía en mí aquí”.

Llevándolo más lejos

Las aserciones de tipo son simples y efectivas, pero hay un problema: TypeScript no realiza ninguna validación en tiempo de ejecución para asegurarse de que su aserción es correcta.Si los datos están en una forma inesperada, o declaró el tipo incorrectamente, es probable que obtenga errores, pero pueden ocurrir lejos de donde afirmó el tipo inicialmente.Esto puede hacer difícil rastrear el problema exacto.

Puede ser factible hacer su propia validación para objetos simples, pero esto se vuelve tedioso rápidamente, especialmente cuando sus objetos se hacen más grandes o tienen algún tipo de anidación.

¿Cómo manejan esto otros idiomas?

Para entender completamente el dilema en el que estamos, es útil ver cómo otros lenguajes estáticos convierten los datos dinámicos en objetos mecanografiados.

Muchos lenguajes, como Java, C# y Go, tienen información de tipos en tiempo de ejecución a la que se puede acceder por reflexión. Estos lenguajes pueden utilizar la información de tipos de las clases para deserializar JSON en objetos bien tipificados.

Lenguajes como Rust tienen macros que pueden generar automáticamente decodificadores para una estructura dada en tiempo de construcción.

Los lenguajes que no tienen ni reflejo, ni macros, típicamente tienen bibliotecas para construir manualmente estos decodificadores.Elm es un gran ejemplo.

TypeScript cae en este último campo de un lenguaje sin reflejos o macros, así que tenemos que ir por la ruta manual.

Decodificación manual

Las dos principales bibliotecas que he visto para escribir estos decodificadores en TypeScript son io-ts y runtypes.

Si vienes de un entorno de programación funcional, probablemente te gusten los io-ts. De lo contrario, puede que encuentres los runtypes más accesibles.

Echemos un breve vistazo a cómo construir decodificadores en los rúnculos:

importar{Record,String,Number}de$0027runtypes$0027constUserRuntype=Record({nombre: String,edad: Número})

Eso es todo. Es casi tan fácil como declarar un tipo de escritura, y nos proporcionará métodos para validar nuestros datos:

importar{Record,String,Number}de$0027runtypes$0027constUserRuntype=Record({nombre: String,age: Number})typeUser={nombre: stringage: number}constrawJson=`{ "nombre": "Alice", "edad": 31}`constuser=parseJson(rawJson)constprintUser=(user: User)={console.log(`User ${user.name} is ${user.age} years old`)}if(UserRuntype.guard(user))printUser(user)

El método de guardia usado al final es un tipo de guardia para comprobar con seguridad si un objeto se ajusta a nuestro tipo. Dentro de la declaración if, el tipo se refina para ser de tipo

{ nombre: cadena, edad: número } – esencialmente el tipo de usuario que definimos anteriormente.

Viendo Doble

Probablemente notaron que básicamente definimos el mismo tipo dos veces:

constUserRuntype=Record({nombre: String,age: Number})typeUser={nombre: stringage: number}

Tener que definir tanto un tipo de TypeScript como un runtype correspondiente no es lo ideal.Por suerte, los runtypes son capaces de derivar un tipo de TypeScript de nuestro runtype de esta manera:

importar{Registro,Cadena,Número,Estático}de$0027tipos de ejecución$0027constUserRuntype=Registro({nombre: Cadena,edad: Número})typeUser=Static<typeofUserRuntype

Ahí lo tienes. Necesitas aprender el DSL de la biblioteca, pero al menos no tienes que definir el tipo dos veces!

Ejemplo completo

Pongámoslo todo junto:

importar{Registro,Cuerda,Número,Estática}de$0027tipos de runas$0027constparseJson=(str: cadena):desconocido==;JSON. parse(str)//expulsar $0027any$0027constUserRuntype=Record({//crear un nombre de runtypeconst: String,age: Number})typeUser=Static<typeofUserRuntype>//derivar un tipo de TypeScript de nuestro runtypeconstprintUser=(user: User)={{console.log(`User ${usuario.nombre} es ${usuario.edad} años de edad`)}constrawJson=`{ "nombre": "Alice", "edad": 31}`constuser=parseJson(rawJson)//el tipo de $0027usuario$0027 es $0027desconocido$0027si(UserRuntype.guard(user))printUser(user)//$0027user$0027 es refinado por nuestro guardia para escribir $0027User$0027

¡Esto parece difícil!

Puede parecer más fácil utilizar un lenguaje dinámico, como JavaScript, pero eso sólo aplaza los posibles errores de tipo al tiempo de ejecución. El tiempo que pase por adelantado siendo explícito acerca de esa estructura con el sistema de tipos pagará dividendos, tanto en el desarrollo inicial como más allá.

El enfoque de afirmación de tipo para escribir sus datos dinámicos es de bajo costo y ciertamente mejor que recurrir a la escritura dinámica2 .

¡Feliz escritura!

  1. También asegúrate de habilitar noImplicitAny en tu tsconfig.json. (O, mejor aún, usa strict, que te dará esto y un montón de otros valores por defecto más seguros). Desafortunadamente, esto no captará todos los casos de cualquiera, como cuando los datos se anotan explícitamente con cualquiera (como JSON.parse).[return]
  2. Si decides seguir la ruta de afirmación de tipo, asegúrate de ejecutar el código para verificar que has afirmado el tipo correctamente. Mejor aún, ¡escriba una prueba![return]

Categorías: technicalTags: javascript, testing