Capítulo 1. Conocer TypeScript

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Antes de sumergirnos en los detalles, este capítulo te ayuda a comprender el panorama general de TypeScript. ¿Qué es y cómo debes pensar en él? ¿Cómo se relaciona con JavaScript? ¿Sus tipos son anulables o no? ¿Qué es eso de any? ¿Y los patos?

TypeScript es un lenguaje inusual en el sentido de que no se ejecuta en un intérprete (como Python y Ruby) ni se compila a un lenguaje de bajo nivel (como Java y C). En su lugar, se compila en otro lenguaje de alto nivel, JavaScript. Es este JavaScript el que se ejecuta, no tu TypeScript. Así que comprender la relación de TypeScript con JavaScript es esencial y te ayudará a ser un desarrollador de TypeScript más eficaz.

El sistema de tipos de TypeScript también tiene algunos aspectos inusuales que debes conocer. En capítulos posteriores se tratará el sistema de tipos con mucho más detalle, pero en éste se tratarán algunos de los aspectos más importantes.

Deberías leer este capítulo aunque ya hayas escrito mucho TypeScript. Te ayudará a construir modelos mentales correctos de lo que es TypeScript y de cómo funciona su sistema de tipos, y puede aclarar algunos conceptos erróneos que no sabías que tenías.

Punto 1: Comprender la relación entre TypeScript y JavaScript

Si utilizas TypeScript durante mucho tiempo, inevitablemente oirás la frase "TypeScript es un superconjunto de JavaScript" o "TypeScript es un superconjunto tipado de JavaScript". Pero, ¿qué significa esto exactamente? ¿Y cuál es la relación entre TypeScript y JavaScript? Dado que estos lenguajes están tan estrechamente vinculados, una sólida comprensión de cómo se relacionan entre sí es la base para utilizar bien TypeScript.

A es un "superconjunto" de B si todo lo que hay en B está también en A. TypeScript es un superconjunto de JavaScript en un sentido sintáctico: siempre que tu programa JavaScript no tenga errores de sintaxis, entonces también es un programa TypeScript. Es muy probable que el comprobador de tipos de TypeScript señale algunos problemas en tu código. Pero éste es un problema independiente. TypeScript seguirá analizando tu código y emitiendo JavaScript. (Esta es otra parte clave de la relación. Exploraremos esto más a fondo en el punto 3).

Los archivos TypeScript utilizan una extensión .ts, en lugar de la extensión .js de un archivo JavaScript.1 ¡Esto no significa que TypeScript sea un lenguaje completamente diferente! Puesto que TypeScript es un superconjunto de JavaScript, el código de tus archivos .js ya es TypeScript. Cambiar el nombre de main.js a main.ts no cambia eso.

Esto es enormemente útil si estás migrando una base de código JavaScript existente a TypeScript. Significa que no tienes que reescribir nada de tu código en otro lenguaje para empezar a utilizar TypeScript y obtener las ventajas que proporciona. Esto no sería así si eligieras reescribir tu JavaScript en un lenguaje como Java. Esta suave ruta de migración es una de las mejores características de TypeScript. Habrá mucho más que decir sobre este tema en el Capítulo 10.

Todos los programas JavaScript son programas TypeScript, pero lo contrario no es cierto: hay programas TypeScript que no son programas JavaScript. Esto se debe a que TypeScript añade sintaxis adicional para especificar tipos. (También añade otras partes de sintaxis, en gran parte por razones históricas. Véase el punto 72.)

Por ejemplo, este es un programa TypeScript válido:

function greet(who: string) {
  console.log('Hello', who);
}

Pero cuando ejecutes esto a través de un programa como node que espera JavaScript, obtendrás un error:

function greet(who: string) {
                  ^

SyntaxError: Unexpected token :

: string es una anotación de tipo específica de TypeScript. Una vez que utilices una, habrás ido más allá del simple JavaScript (véase la Figura 1-1).

ets2 0101
Figura 1-1. Todo JavaScript es TypeScript, pero no todo TypeScript es JavaScript.

Esto no quiere decir que TypeScript no aporte valor a los programas JavaScript simples. Sí que lo hace. Por ejemplo, este programa JavaScript:

let city = 'new york city';
console.log(city.toUppercase());

arrojará un error cuando lo ejecutes:

TypeError: city.toUppercase is not a function

No hay anotaciones de tipo en este programa, pero el comprobador de tipos de TypeScript es capaz de detectar el problema:

let city = 'new york city';
console.log(city.toUppercase());
//               ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
//                           'string'. Did you mean 'toUpperCase'?

No tuviste que decirle a TypeScript que el tipo de city era string: lo dedujo del valor inicial . La inferencia de tipos es una parte clave de TypeScript, y el Capítulo 3 explora cómo utilizarla bien.

Uno de los objetivos de del sistema de tipos de TypeScript es detectar el código que lanzará una excepción en tiempo de ejecución, sin tener que ejecutar tu código. Cuando oigas describir TypeScript como un sistema de tipos "estático", te estarás refiriendo a esta capacidad. El comprobador de tipos no siempre puede detectar el código que lanzará excepciones, pero lo intentará.

Aunque tu código no lance una excepción, puede que no haga lo que pretendías. TypeScript también intenta detectar algunos de estos problemas. Por ejemplo, esteprograma JavaScript:

const states = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capital: 'Juneau'},
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];
for (const state of states) {
  console.log(state.capitol);
}

registrará:

undefined
undefined
undefined

¡Uy! ¿Qué ha salido mal? Este programa es JavaScript válido (y por tanto TypeScript). Y se ejecutó sin lanzar ningún error. Pero está claro que no hizo lo que pretendías. Incluso sin añadir anotaciones de tipo, el comprobador de tipos de TypeScript es capaz de detectar el error y ofrecer una sugerencia útil:

for (const state of states) {
  console.log(state.capitol);
  //                ~~~~~~~ Property 'capitol' does not exist on type
  //                        '{ name: string; capital: string; }'.
  //                        Did you mean 'capital'?
}

De hecho, queríamos decir capital con "a". Los estados y los países tienen capitales ("a"), mientras que las asambleas legislativas se reúnen en capitolios ("o").

Aunque TypeScript puede detectar errores incluso si no proporcionas anotaciones de tipo, es capaz de hacer un trabajo mucho más exhaustivo si lo haces. Esto se debe a que las anotaciones de tipo le dicen a TypeScript cuál es tu intención, y esto le permite detectar lugares en los que el comportamiento de tu código no coincide con tu intención. Por ejemplo, ¿qué pasaría si hubieras invertido el error ortográfico capital/capitol del ejemplo anterior?

const states = [
  {name: 'Alabama', capitol: 'Montgomery'},
  {name: 'Alaska',  capitol: 'Juneau'},
  {name: 'Arizona', capitol: 'Phoenix'},
  // ...
];
for (const state of states) {
  console.log(state.capital);
  //                ~~~~~~~ Property 'capital' does not exist on type
  //                        '{ name: string; capitol: string; }'.
  //                        Did you mean 'capitol'?
}

El error que antes era tan útil, ¡ahora lo hace exactamente mal! El problema es que has escrito la misma propiedad de dos formas distintas, y TypeScript no sabe cuál es la correcta. Puede adivinarlo, pero no siempre acierta. La solución es aclarar tu intención declarando explícitamente el tipo de states:

interface State {
  name: string;
  capital: string;
}
const states: State[] = [
  {name: 'Alabama', capitol: 'Montgomery'},
  //                ~~~~~~~
  {name: 'Alaska',  capitol: 'Juneau'},
  //                ~~~~~~~
  {name: 'Arizona', capitol: 'Phoenix'},
  //                ~~~~~~~ Object literal may only specify known properties,
  //                        but 'capitol' does not exist in type 'State'.
  //                        Did you mean to write 'capital'?
  // ...
];
for (const state of states) {
  console.log(state.capital);
}

Ahora los errores coinciden con el problema y la solución sugerida es correcta. Al explicar tu intención, también has ayudado a TypeScript a detectar otros problemas potenciales. Por ejemplo, si sólo hubieras escrito mal capitol una vez en la matriz, antes no habría habido ningún error. Pero con la anotación de tipo, sí lo hay:

const states: State[] = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capitol: 'Juneau'},
  //                ~~~~~~~ Did you mean to write 'capital'?
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];

Esto se convertirá en en una dinámica familiar a medida que trabajes con el corrector tipográfico: cuanta más información le des, más problemas podrá encontrar.

En términos del diagrama de Venn, podemos añadir un nuevo grupo de programas: Los programas TypeScript que pasan el comprobador de tipos (ver Figura 1-2).

ets2 0102
Figura 1-2. Todos los programas JavaScript son programas TypeScript. Pero sólo algunos programas JavaScript (y TypeScript) pasan el comprobador de tipos.

Si la afirmación de que "TypeScript es un superconjunto de JavaScript" te parece incorrecta, puede deberse a que estás pensando en este tercer conjunto de programas del diagrama. En la práctica, éste es el más relevante para la experiencia cotidiana de utilizar TypeScript. Generalmente, cuando utilizas TypeScript, intentas que tu código pase todas las comprobaciones de tipo.

El sistema de tipos de TypeScript modela el comportamiento en tiempo de ejecución de JavaScript. Esto puede dar lugar a algunas sorpresas si vienes de un lenguaje con comprobaciones en tiempo de ejecución más estrictas. Por ejemplo:

const x = 2 + '3';  // OK
//    ^? const x: string
const y = '2' + 3;  // OK
//    ^? const y: string

Ambas expresiones superan el comprobador de tipos, aunque son cuestionables y producen errores en tiempo de ejecución en muchos otros lenguajes. Pero esto modela con precisión el comportamiento en tiempo de ejecución de JavaScript, donde ambas expresiones dan como resultado la cadena "23".

Sin embargo, TypeScript traza la línea en alguna parte. El verificador de tipos señala problemas en todas estas sentencias, aunque no lancen excepciones en tiempo de ejecución:

const a = null + 7;  // Evaluates to 7 in JS
//        ~~~~ The value 'null' cannot be used here.
const b = [] + 12;  // Evaluates to '12' in JS
//        ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript');  // alerts "Hello"
//             ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2

El principio rector del sistema de tipos de TypeScript es que debe modelar el comportamiento en tiempo de ejecución de JavaScript. Pero en todos estos casos, TypeScript considera más probable que el uso extraño sea el resultado de un error que la intención del desarrollador, por lo que va más allá de simplemente modelar el comportamiento en tiempo de ejecución. Vimos otro ejemplo de esto en el ejemplo capital/capitol, en el que el programa no lanzaba (registraba undefined) pero el comprobador de tipos seguía marcando un error.

¿Cómo decide TypeScript cuándo modelar el comportamiento en tiempo de ejecución de JavaScript y cuándo ir más allá? En última instancia, se trata de una cuestión de gustos. Al adoptar TypeScript estás confiando en el juicio del equipo que lo construye. Si disfrutas añadiendo null y 7 o [] y 12, o llamando a funciones con argumentos superfluos, ¡puede que TypeScript no sea para ti!

Si tu programa comprueba el tipo, ¿podría seguir lanzando un error en tiempo de ejecución? La respuesta es "sí". He aquí un ejemplo:

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());

Cuando ejecutas esto, lanza:

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

TypeScript asumió que el acceso a la matriz estaría dentro de los límites, pero no fue así. El resultado fue una excepción.

También es frecuente que se produzcan errores involuntarios cuando utilizas el tipo any, de lo que hablaremos en el Tema 5 y con más detalle en el Capítulo 5.

La causa fundamental de estas excepciones es que la comprensión que TypeScript tiene del tipo de un valor (su tipo estático) y su tipo real en tiempo de ejecución han divergido. Se dice que un sistema de tipos que puede garantizar la exactitud de sus tipos estáticos es sólido. El sistema de tipos de TypeScript no es sólido, ni nunca se pretendió que lo fuera. El punto 48 explora más formas en las que puede surgir la falta de solidez.

Si la solidez es importante para ti, puede que quieras buscar otros lenguajes como Reason, PureScript o Dart. Aunque éstos ofrecen más garantías de seguridad en tiempo de ejecución, tienen un coste: cuesta más trabajo convencer a sus comprobadores de tipos de que tu código es correcto, y ninguno de ellos es un superconjunto de JavaScript, por lo que la migración será más complicada.

Cosas para recordar

  • TypeScript es un superconjunto de JavaScript: todos los programas JavaScript son programas TypeScript sintácticamente válidos, pero no todos los programas TypeScript son programas JavaScript válidos.

  • TypeScript añade un sistema de tipos estáticos que modela el comportamiento en tiempo de ejecución de JavaScript e intenta detectar el código que lanzará excepciones en tiempo de ejecución.

  • Es posible que el código pase el comprobador de tipos pero siga lanzando en tiempo de ejecución.

  • TypeScript no permite algunas construcciones JavaScript legales pero cuestionables, como llamar a funciones con un número incorrecto de argumentos.

  • Las anotaciones de tipo indican a TypeScript tu intención y le ayudan a distinguir el código correcto del incorrecto.

Punto 2: Saber qué opciones de TypeScript estás utilizando

¿Pasa este código el comprobador de tipos?

function add(a, b) {
  return a + b;
}
add(10, null);

Sin saber qué opciones estás utilizando, ¡es imposible saberlo! El compilador de TypeScript tiene un enorme conjunto de ellas, más de cien en el momento de escribir esto.

Se pueden establecer mediante la línea de comandos:

$ tsc --noImplicitAny program.ts

o mediante un archivo de configuración, tsconfig.json:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Deberías preferir el archivo de configuración. Garantiza que todos tus colaboradores y herramientas sepan exactamente cómo piensas utilizar TypeScript. Puedes crear uno ejecutando tsc --init.

Muchos de los ajustes de configuración de TypeScript controlan dónde busca los archivos fuente y qué tipo de salida genera. Pero unos pocos controlan aspectos fundamentales del propio lenguaje. Se trata de opciones de diseño de alto nivel que la mayoría de los lenguajes no dejan en manos de sus usuarios. TypeScript puede parecer un lenguaje muy diferente dependiendo de cómo esté configurado. Para utilizarlo eficazmente, debes entender las más importantes de estas configuraciones: noImplicitAny y strictNullChecks.

noImplicitAny

noImplicitAny controla lo que hace TypeScript cuando no puede determinar el tipo de una variable. Este código es válido cuando noImplicitAny está desactivado:

function add(a, b) {
  return a + b;
}

Si pasas el ratón por encima del símbolo add en tu editor, te revelará lo que TypeScript ha deducido sobre el tipo de esa función:

function add(a: any, b: any): any

Los tipos any desactivan de hecho el comprobador de tipos para el código en el que intervienen estos parámetros. any es una herramienta útil, pero debe utilizarse con precaución. Para más información sobre any, consulta el Tema 5 y el Capítulo 5.

Éstos se denominan implícitos any s porque nunca escribiste la palabra "any" pero aún así acabaste con tipos peligrosos any. Esto se convierte en un error si activas la opción noImplicitAny:

function add(a, b) {
  //         ~    Parameter 'a' implicitly has an 'any' type
  //            ~ Parameter 'b' implicitly has an 'any' type
  return a + b;
}

Estos errores pueden solucionarse escribiendo explícitamente declaraciones de tipo, ya sea : any o un tipo más específico:

function add(a: number, b: number) {
  return a + b;
}

TypeScript es más útil cuando tiene información sobre tipos, por lo que debes asegurarte de establecer noImplicitAny siempre que sea posible. Una vez que te acostumbras a que todas las variables tengan tipos, TypeScript sin noImplicitAny se siente casi como un lenguaje diferente .

Para proyectos nuevos, deberías empezar con noImplicitAny activado, de modo que escribas los tipos a medida que escribes tu código. Esto ayudará a TypeScript a detectar problemas, mejorará la legibilidad de tu código y mejorará tu experiencia de desarrollo (véase el punto 6).

Dejar noImplicitAny desactivado sólo es apropiado si estás haciendo la transiciónde un proyectode JavaScript a TypeScript (ver Capítulo 10). Incluso entonces, sólo debería ser un estado temporal y deberías activarlo lo antes posible. TypeScript sinnoImplicitAny es sorprendentemente flojo. El tema 83 explora cómo esto puede llevar a a tener problemas.

strictNullChecks

strictNullChecks controla si null y undefined son valores permitidos en cada tipo.

Este código es válido cuando strictNullChecks está apagado:

const x: number = null;  // OK, null is a valid number

pero provoca un error cuando enciendes strictNullChecks:

const x: number = null;
//    ~ Type 'null' is not assignable to type 'number'

Se habría producido un error similar si hubieras utilizado undefined en lugar de null.

Si tu intención es permitir null, puedes corregir el error haciendo explícita tu intención:

const x: number | null = null;

Si no deseas permitir null, tendrás que localizar su procedencia y añadir un cheque o una afirmación:

const statusEl = document.getElementById('status');
statusEl.textContent = 'Ready';
// ~~~~~ 'statusEl' is possibly 'null'.

if (statusEl) {
  statusEl.textContent = 'Ready';  // OK, null has been excluded
}
statusEl!.textContent = 'Ready';  // OK, we've asserted that el is non-null

Utilizar una afirmación if de este modo se conoce como "estrechar" o "refinar" un tipo, y este patrón se explora en el Tema 22. El "!" de la última línea se denomina "aserción no nula". Las aserciones de tipo tienen su lugar en TypeScript, pero también pueden provocar excepciones en tiempo de ejecución. El Tema 9 explicará cuándo debes y cuándo no debes utilizar una aserción de tipo.

strictNullChecks es tremendamente útil para detectar errores relacionados con los valores null y undefined, pero aumenta la dificultad de uso del lenguaje. Si estás empezando un nuevo proyecto y ya has utilizado TypeScript antes, establece strictNullChecks. Pero si eres nuevo en el lenguaje o estás migrando una base de código JavaScript, puedesoptar por dejarlo desactivado. Por supuesto, debes configurar noImplicitAny antes de configurarstrictNullChecks.

Si decides trabajar sin strictNullChecks, estate atento al temido error de ejecución "undefined is not an object". Cada uno de ellos es un recordatorio de que deberías plantearte activar una comprobación más estricta. Cambiar esta configuración sólo será más difícil a medida que tu proyecto crezca, así que intenta no esperar demasiado antes de activarla. La mayor parte del código TypeScript utiliza strictNullChecks, y aquí es donde finalmente querrás estar .

Otras opciones

Hay muchos otros ajustes que afectan a la semántica del lenguaje (por ejemplo, noImplicitThis y strictFunctionTypes), pero éstos son menores en comparación con noImplicitAny y strictNullChecks. Para activar todas estas comprobaciones, activa el ajuste strict. TypeScript es capaz de detectar la mayoría de los errores con strict, por lo que éste debería ser tu objetivo.

Si creas un proyecto utilizando tsc --init, estarás por defecto en el modo strict.

También hay disponibles algunas opciones "más estrictas que estrictas". Puedes optar por ellas para que TypeScript sea aún más agresivo a la hora de encontrar errores en tu código. Una de estas opciones es noUncheckedIndexedAccess, que ayuda a detectar errores en el acceso a objetos y matrices. Por ejemplo, este código no tiene errores de tipo en --strict pero lanza una excepción en tiempo de ejecución:

const tenses = ['past', 'present', 'future'];
tenses[3].toUpperCase();

Con noUncheckedIndexedAccess activado, esto es un error:

const tenses = ['past', 'present', 'future'];
tenses[3].toUpperCase();
// ~~~~~~ Object is possibly 'undefined'.

Sin embargo, esto no es un almuerzo gratis. Muchos accesos válidos también se marcarán como posibles undefined:

tenses[0].toUpperCase();
// ~~~~~~ Object is possibly 'undefined'.

Algunos proyectos TypeScript utilizan esta configuración, mientras que otros no. Al menos deberías saber que existe. Habrá más que decir sobre este ajuste en el Tema 48.

¡Conoce las opciones que estás utilizando! Si un compañero de trabajo comparte un ejemplo de TypeScript y no puedes reproducir sus errores, asegúrate de que tus opciones de compilador son las mismas.

Cosas para recordar

  • El compilador de TypeScript incluye varios ajustes que afectan a aspectos fundamentales del lenguaje.

  • Configura TypeScript utilizando tsconfig.json en lugar de las opciones de la línea de comandos.

  • Activa noImplicitAny a menos que estés pasando un proyecto JavaScript aTypeScript.

  • Utiliza strictNullChecks para evitar errores de ejecución del tipo "indefinido no es un objeto".

  • Su objetivo es permitir que strict obtenga la comprobación más exhaustiva que TypeScript puede ofrecer.

Tema 3: Comprender que la generación de código es independiente de los tipos

A un alto nivel , tsc (el compilador de TypeScript) hace dos cosas:

  • Convierte TypeScript/JavaScript de última generación en una versión antigua de JavaScript que funcione en navegadores u otros tiempos de ejecución ("transpiling").

  • Comprueba si hay errores de tipo en tu código.

Lo sorprendente es que estos dos comportamientos son totalmente independientes entre sí. Dicho de otro modo, los tipos de tu código no pueden afectar al JavaScript que emite TypeScript. Puesto que es este JavaScript el que se ejecuta, esto significa que tus tipos no pueden afectar a la forma en que se ejecuta tu código.

Esto tiene algunas implicaciones sorprendentes y debería informar tus expectativas sobre lo que TypeScript puede y no puede hacer por ti.

No puedes comprobar los tipos TypeScript en tiempo de ejecución

puedes tener la tentación de escribir un código como éste:

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    //                 ~~~~~~~~~ 'Rectangle' only refers to a type,
    //                           but is being used as a value here
    return shape.height * shape.width;
    //           ~~~~~~ Property 'height' does not exist on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}

La comprobación instanceof se produce en tiempo de ejecución, pero Rectangle es un tipo, por lo que no puede afectar al comportamiento en tiempo de ejecución del código. Los tipos TypeScript son "borrables": parte de la compilación a JavaScript consiste simplemente en eliminar todas las interfaces, types y anotaciones de tipo de tu código. Esto es más fácil de ver si observas el JavaScript al que se compila esta muestra:

function calculateArea(shape) {
  if (shape instanceof Rectangle) {
    return shape.height * shape.width;
  } else {
    return shape.width * shape.width;
  }
}

Aquí no se menciona Rectangle antes de la comprobación de instanceof, de ahí el problema de .2 Para saber con qué tipo de forma estás tratando, necesitarás alguna forma de reconstruir su tipo en tiempo de ejecución, es decir, alguna forma que tenga sentido en el JavaScript generado, no sólo en el TypeScript original.

Hay varias formas de hacerlo. Una es comprobar la presencia de unapropiedad height:

function calculateArea(shape: Shape) {
  if ('height' in shape) {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
  }
}

Esto funciona porque la comprobación de propiedades sólo afecta a los valores disponibles en tiempo de ejecución, pero sigue permitiendo que el comprobador de tipos refine el tipo de shapea Rectangle.

Otra forma sería introducir una "etiqueta" para almacenar explícitamente el tipo de forma que esté disponible en tiempo de ejecución:

interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
    //     ^? (parameter) shape: Square
  }
}

Aquí la propiedad kind actúa como "etiqueta", y decimos que el tipo Shape es una "unión etiquetada". A veces también se denomina "unión discriminada", en cuyo caso kind es el "discriminante". Los términos son intercambiables. Dado que facilitan la recuperación de la información de tipo en tiempo de ejecución, las uniones etiquetadas/discriminadas son omnipresentes en TypeScript.

Algunas construcciones introducen tanto un tipo (que no está disponible en tiempo de ejecución) como un valor (que sí lo está). La palabra clave class es una de ellas. Crear las clases Square y Rectangle sería otra forma de solucionar el error:

class Square {
  width: number;
  constructor(width: number) {
    this.width = width;
  }
}
class Rectangle extends Square {
  height: number;
  constructor(width: number, height: number) {
    super(width);
    this.height = height;
  }
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
    //     ^? (parameter) shape: Square
  }
}

Esto funciona porque class Rectangle introduce tanto un tipo como un valor, mientras que interface sólo introduce un tipo.

El Rectangle en type Shape = Square | Rectangle se refiere al tipo, pero el Rectangle en shape instanceof Rectangle se refiere al valor, en este caso la función constructora. Esta distinción es importante de entender, pero puede ser bastante sutil. El punto 8 te muestra cómo decir a cuál es cuál.

El código con errores tipográficos puede producir resultados

Como la salida de código de es independiente de la comprobación de tipos, ¡se deduce que el código con errores de tipo puede producir salida!

$ cat test.ts
let x = 'hello';
x = 1234;
$ tsc test.ts
test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string'

2 x = 1234;
  ~

$ cat test.js
var x = 'hello';
x = 1234;

Esto puede ser bastante sorprendente si vienes de un lenguaje como C o Java, donde la comprobación de tipos y la salida van de la mano. Puedes pensar que todos los errores de TypeScript son similares a las advertencias en esos lenguajes: es probable que indiquen un problema y merezca la pena investigarlo, pero no detendrán la compilación.

La emisión de código en presencia de errores es útil en la práctica. Si estás construyendo una aplicación web, puede que sepas que hay problemas con una parte concreta de ella. Pero como TypeScript seguirá generando código en presencia de errores, puedes probar las otras partes de tu aplicación antes de arreglarlas.

Deberías aspirar a cero errores cuando confirmes el código, para no caer en la trampa de tener que recordar qué es un error esperado o inesperado. Si quieres desactivar la salida de errores, puedes utilizar la opción noEmitOnError en tsconfig.json, o su equivalente en tu herramienta de compilación .

Las operaciones de tipo no pueden afectar a los valores en tiempo de ejecución

Supongamos que tienes un valor que podría ser una cadena o un número y te gustaría normalizarlo para que siempre sea un número. He aquí un intento erróneo que el comprobador de tipos acepta:

function asNumber(val: number | string): number {
  return val as number;
}

Observando el JavaScript generado queda claro lo que hace realmente esta función:

function asNumber(val) {
  return val;
}

No se produce ningún tipo de conversión. El as number es una operación de tipo, por lo que no puede afectar al comportamiento en tiempo de ejecución de tu código. Para normalizar el valor tendrás que comprobar su tipo en tiempo de ejecución y hacer la conversión utilizando construcciones JavaScript:

function asNumber(val: number | string): number {
  return Number(val);
}

"as number" es una aserción de tipo, a veces llamada inexactamente "cast". Para saber más sobre cuándo es apropiado utilizar aserciones de tipo, consulta el Tema 9.

Los tipos en tiempo de ejecución pueden no ser los mismos que los tipos declarados

¿Podría esta función llegar alguna vez a la final console.log?

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log(`I'm afraid I can't do that.`);
  }
}

TypeScript suele marcar el código muerto, pero no se queja de esto, ni siquiera con la opción strict. ¿Cómo has podido dar con esta rama?

La clave está en recordar que boolean es el tipo declarado. Como es un tipo TypeScript, desaparece en tiempo de ejecución. En el código JavaScript, un usuario podría llamar inadvertidamente asetLightSwitch con un valor como "ON".

También hay formas de activar esta ruta de código en TypeScript puro. Tal vez se llame a la función con un valor procedente de una llamada de red:

interface LightApiResponse {
  lightSwitchValue: boolean;
}
async function setLight() {
  const response = await fetch('/light');
  const result: LightApiResponse = await response.json();
  setLightSwitch(result.lightSwitchValue);
}

Has declarado que el resultado de la petición /light es LightApiResponse, pero nada lo impone. Si has entendido mal la API y lightSwitchValue es en realidad un string, entonces se pasará una cadena a setLightSwitch en tiempo de ejecución. O puede que la API haya cambiado después de tu implementación.

TypeScript puede volverse bastante confuso cuando los tipos en tiempo de ejecución no coinciden con los tipos declarados, y debes evitar estos tipos llamados "no sólidos" siempre que puedas. Pero ten en cuenta que es posible que un valor tenga un tipo en tiempo de ejecución distinto del que has declarado. Para más información sobre la solidez, consulta el Tema 48.

No puedes sobrecargar una función basada en tipos TypeScript

Los lenguajes como C++ te permiten definir múltiples versiones de una función que sólo difieren en los tipos de sus parámetros. Esto se llama "sobrecarga de funciones". Dado que el comportamiento en tiempo de ejecución de tu código es independiente de sus tipos TypeScript, esta construcción no es posible en TypeScript:

function add(a: number, b: number) { return a + b; }
//       ~~~ Duplicate function implementation
function add(a: string, b: string) { return a + b; }
//       ~~~ Duplicate function implementation

TypeScript ofrece la posibilidad de sobrecargar funciones, pero funciona totalmente a nivel de tipo. Puedes proporcionar varias firmas de tipo para una función, pero sólo una implementación:

function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a: any, b: any) {
  return a + b;
}

const three = add(1, 2);
//    ^? const three: number
const twelve = add('1', '2');
//    ^? const twelve: string

Las dos primeras firmas de add sólo proporcionan información de tipo. Cuando TypeScript produce una salida JavaScript, se eliminan y sólo queda la implementación. Los parámetros any de la implementación no son geniales. Exploraremos cómo manejarlos en el Tema 52, que también cubre algunas sutilezas a tener en cuenta con las sobrecargas de funciones TypeScript.

Los tipos de TypeScript no afectan al rendimiento en tiempo de ejecución

Como los tipos y las operaciones de tipos de se borran cuando generas JavaScript, no pueden tener efecto sobre el rendimiento en tiempo de ejecución. Los tipos estáticos de TypeScript son realmente de coste cero. La próxima vez que alguien ofrezca la sobrecarga en tiempo de ejecución como razón para no usar TypeScript, ¡sabrás exactamente lo bien que ha probado esta afirmación!

Hay dos advertencias al respecto:

  • Aunque no hay sobrecarga en tiempo de ejecución, el compilador de TypeScript introducirá sobrecarga en tiempo de compilación. El equipo de TypeScript se toma muy en serio el rendimiento del compilador y la compilación suele ser bastante rápida, especialmente para compilaciones incrementales. Si la sobrecarga es significativa, tu herramienta de compilación puede tener una opción de "sólo transpilar" para omitir la comprobación de tipos. Habrá más que decir sobre el rendimiento del compilador en el Tema 78.

  • El código que TypeScript emite para soportar tiempos de ejecución más antiguos puede incurrir en una sobrecarga de rendimiento frente a las implementaciones nativas. Por ejemplo, si utilizas funciones de generador y tienes como objetivo ES5, que es anterior a los generadores, tsc emitirá código de ayuda para que las cosas funcionen. Esto supondrá cierta sobrecarga frente a una implementación nativa de generadores. Esto ocurre con cualquier "transpilador" de JavaScript, no sólo con TypeScript. En cualquier caso, esto tiene que ver con el objetivo de emisión y los niveles del lenguaje y sigue siendo independiente de los tipos.

Cosas para recordar

  • La generación de código es independiente del sistema de tipos. Esto significa que los tipos de TypeScript no pueden afectar al comportamiento en tiempo de ejecución de tu código.

  • Es posible que un programa con errores de tipo produzca código ("compile").

  • Los tipos TypeScript no están disponibles en tiempo de ejecución. Para consultar un tipo en tiempo de ejecución, necesitas alguna forma de reconstruirlo. Las uniones etiquetadas y la comprobación de propiedades son formas habituales de hacerlo.

  • Algunas construcciones, como class, introducen tanto un tipo TypeScript como un valor que está disponible en tiempo de ejecución.

  • Dado que se borran como parte de la compilación, los tipos TypeScript no pueden afectar al rendimiento en tiempo de ejecución de tu código.

Tema 4: Siéntete cómodo con la tipificación estructural

JavaScript fomenta la "tipificación pato": si pasas a una función un valor con todas las propiedades correctas, no le importará cómo has hecho el valor. Simplemente lo utilizará. (Este término hace referencia al dicho: "Si camina como un pato y habla como un pato, probablemente sea un pato").

TypeScript modela este comportamiento utilizando lo que se conoce como un sistema de tipos estructural. Esto a veces puede conducir a resultados sorprendentes, porque la comprensión de un tipo por parte del comprobador de tipos puede ser más amplia de lo que tenías en mente. Tener un buen conocimiento del tipado estructural te ayudará a entender los errores y los no errores y a escribir código más robusto.

Digamos que estás trabajando en una biblioteca de física y tienes un tipo de vector 2D:

interface Vector2D {
  x: number;
  y: number;
}

Escribe una función para calcular su longitud:

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x ** 2 + v.y ** 2);
}

Ahora introduces la noción de vector con nombre:

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

La función calculateLength funcionará con NamedVectors porque tienen propiedades x y y que son numbers. TypeScript es lo suficientemente inteligente como para darse cuenta de esto:

const v: NamedVector = { x: 3, y: 4, name: 'Pythagoras' };
calculateLength(v);  // OK, result is 5

Lo interesante es que nunca declaraste la relación entre Vector2D y NamedVector. Y no tuviste que escribir una implementación alternativa de calculateLength para NamedVectors. El sistema de tipos de TypeScript está modelando el comportamiento en tiempo de ejecución de JavaScript(punto 1). Permitía llamar a calculateLength con unNamedVector porque su estructura era compatible con Vector2D. De ahí viene el término "tipado estructural".

Pero esto también puede acarrear problemas. Digamos que añades un tipo de vector 3D:

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

y escribe una función para normalizarlos (hacer que su longitud sea 1):

function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

Si llamas a esta función, es probable que obtengas un vector con una longitud superior a 1:

> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1 }

Este vector tiene una longitud de alrededor de 1,4, no de 1. Entonces, ¿qué falló y por qué TypeScript no detectó el error?

El error es que calculateLength opera con vectores 2D, pero normalize opera con vectores 3D. Así que el componente z se ignora en la normalización.

Lo que quizá sea más sorprendente es que el comprobador de tipos no detecte este problema. ¿Por qué se permite llamar a calculateLength con un vector 3D, a pesar de que su declaración de tipo dice que toma vectores 2D?

Lo que funcionaba tan bien con los vectores con nombre, aquí ha resultado contraproducente. Llamar a calculateLength con un objeto {x, y, z} no produce ningún error. Así que el verificador de tipos tampoco se queja, y este comportamiento ha dado lugar a un error.

(Si quieres que esto sea un error, tienes algunas opciones. El punto 63 presenta un truco para prohibir específicamente la propiedad z, y el punto 64 muestra cómo puedes utilizar "marcas" para evitar por completo este tipo de tipado estructural).

Cuando escribes funciones, es fácil imaginar que serán llamadas con argumentos que tengan las propiedades que has declarado y ninguna otra. Esto se conoce como un tipo "cerrado", "sellado" o "preciso", y no puede expresarse en el sistema de tipos de TypeScript. Te guste o no, tus tipos son "abiertos".

Esto a veces puede dar lugar a sorpresas:

function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
    //            ~~~~~~~ Element implicitly has an 'any' type because ...
    //                    'string' can't be used to index type 'Vector3D'
    length += Math.abs(coord);
  }
  return length;
}

¿Por qué es un error? Dado que axis es una de las claves de v, que es un Vector3D, debería ser "x", "y" o "z". Y según la declaración de Vector3D, todos ellos son numbers , por lo que ¿el tipo de coord no debería ser number?

¿Este error es un falso positivo? ¡No! TypeScript se queja correctamente. La lógica del párrafo anterior supone que Vector3D está sellado y no tiene otras propiedades. Pero podría tenerlas:

const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D);  // OK, returns NaN

Puesto que v podría tener cualquier propiedad, el tipo de axis es string. TypeScript no tiene motivos para creer que v[axis] es un número porque, como acabas de ver, podría no serlo. (La variable vec3D evita aquí el exceso de comprobación de propiedades, que es el tema del Tema 11).

Iterar sobre objetos puede ser complicado de escribir correctamente. Volveremos sobre este tema en el Tema 60, pero en este caso sería mejor una implementación sin bucles:

function calculateLengthL1(v: Vector3D) {
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}

La tipificación estructural también puede dar lugar a sorpresas con classes, que se comparan estructuralmente para asignabilidad:

class SmallNumContainer {
  num: number;
  constructor(num: number) {
    if (num < 0 || num >= 10) {
      throw new Error(`You gave me ${num} but I want something 0-9.`)
    }
    this.num = num;
  }
}

const a = new SmallNumContainer(5);
const b: SmallNumContainer = { num: 2024 };  // OK!

¿Por qué b es asignable a SmallNumContainer? Tiene una propiedad num que es un number. Así que las estructuras coinciden. Esto podría acarrear problemas si escribes una función que asuma que se ha ejecutado la lógica de validación en el constructor de SmallNumContainer. Es menos probable que esto ocurra por casualidad en clases con más propiedades y métodos, pero es muy diferente de lenguajes como C++ o Java, donde declarar un parámetro de tipo SmallNumContainer garantiza que será SmallNumContainer o una subclase suya, y por tanto que se habrá ejecutado la lógica de validación en el constructor.

La tipificación estructural es beneficiosa cuando escribes pruebas. Digamos que tienes una función que ejecuta una consulta en una base de datos y procesa los resultados:

interface Author {
  first: string;
  last: string;
}
function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

Para probarlo, podrías crear un simulacro PostgresDB. Pero un enfoque más sencillo es utilizar la tipificación estructural y definir una interfaz más estrecha:

interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

Todavía puedes pasar a getAuthors un PostgresDB en producción, ya que tiene un método runQuery. Debido al tipado estructural, el PostgresDB no necesita decir que implementa DB. TypeScript lo deducirá.

Cuando escribas tus pruebas, puedes pasar en su lugar un objeto más sencillo:

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });
  expect(authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);
});

TypeScript verificará que nuestra prueba DB se ajusta a la interfaz. Y tus pruebas no necesitan saber nada de tu base de datos de producción: ¡no hacen falta bibliotecas de imitación! Al introducir una abstracción (DB), hemos liberado nuestra lógica (y nuestras pruebas) de los detalles de una implementación específica de (PostgresDB).

Otra ventaja del tipado estructural es que puede cortar limpiamente las dependencias entre bibliotecas. Para saber más sobre esto, consulta el Tema 70.

Cosas para recordar

  • Comprende que JavaScript es de tipado denso y TypeScript utiliza el tipado estructural para modelarlo: los valores asignables a tus interfaces pueden tener propiedades más allá de las explícitamente enumeradas en tus declaraciones de tipos. Los tipos no están "sellados".

  • Ten en cuenta que las clases también siguen reglas de tipado estructural. ¡Puede que no tengas una instancia de la clase que esperas!

  • Utiliza el tipado estructural para facilitar las pruebas unitarias de .

Tema 5: Limitar el uso de cualquier tipo

El sistema de tipos de TypeScript es gradual y opcional: gradual porque puedes añadir tipos a tu código poco a poco (con noImplicitAny), y opcional porque puedes desactivar el comprobador de tipos cuando quieras. La clave de estas características es el tipo any:

let ageInYears: number;
ageInYears = '12';
// ~~~~~~~ Type 'string' is not assignable to type 'number'.
ageInYears = '12' as any;  // OK

El comprobador de tipos tiene razón al quejarse aquí, pero puedes silenciarlo escribiendo as any. Cuando empiezas a usar TypeScript, es tentador usar tipos de any y aserciones de tipos (as any) cuando no entiendes un error, crees que el comprobador de tipos es incorrecto o simplemente no quieres tomarte el tiempo de escribir declaraciones de tipos.

En algunos casos puede estar bien, pero ten en cuenta que utilizar any elimina muchas de las ventajas de utilizar TypeScript. Al menos deberías comprender sus peligros antes de utilizarlo.

No hay seguridad tipográfica con ningún tipo

En el ejemplo anterior, la declaración de tipos dice que ageInYears es un number. Pero any te permite asignarle un string. El verificador de tipos creerá que es un number (al fin y al cabo, eso es lo que has dicho), y el caos no se detectará:

ageInYears += 1;  // OK; at runtime, ageInYears is now "121"

any te permite romper contratos

Cuando escribes una función, estás especificando un contrato: si quien te llama te da un determinado tipo de entrada, producirás un determinado tipo de salida. Pero con un tipo any, puedes romper estos contratos:

function calculateAge(birthDate: Date): number {
  // ...
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // OK

El parámetro de la fecha de nacimiento debería ser un Date, no un string. El tipo any te ha permitido romper el contrato de calculateAge. Esto puede ser especialmente problemático porque JavaScript a menudo está dispuesto a convertir implícitamente entre tipos. Un string funcionará a veces donde se espera un number, sólo para romperse en otras circunstancias.

No hay servicios lingüísticos para ningún tipo

Cuando una variable tiene un tipo que no esany, los servicios del lenguaje TypeScriptpueden proporcionar autocompletado inteligente y documentación contextual (como se muestra en la Figura 1-3).

ets2 0103
Figura 1-3. El Servicio del Lenguaje TypeScript es capaz de proporcionar autocompletado contextual para símbolos con tipos.

Pero para los símbolos con un tipo any, estás solo(Figura 1-4).

ets2 0104
Figura 1-4. No hay autocompletado para las propiedades de los símbolos con tipos any.

Renombrar es otro de estos servicios. Si tienes un tipo Person y funciones para formatear el nombre de una persona:

interface Person {
  first: string;
  last: string;
}

const formatName = (p: Person) => `${p.first} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;

entonces puedes seleccionar first en tu editor, elegir "Cambiar nombre de símbolo" y cambiarlo a firstName (ver Figuras 1-5 y 1-6).

ets2 0105
Figura 1-5. Cambiar el nombre de un símbolo en VS Code.
ets2 0106
Figura 1-6. Elegir el nuevo nombre.

Esto modifica la función formatName pero no la versión any:

interface Person {
  firstName: string;
  last: string;
}
const formatName = (p: Person) => `${p.firstName} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;

Uno de los lemas de TypeScript es "JavaScript que escala". Una parte clave de "escala" son los servicios del lenguaje, que son una parte esencial de la experiencia TypeScript (ver punto 6). Perderlos conllevará una pérdida de productividad, no sólo para ti, sino para todos los que trabajen con tu código.

ualquier Tipo Enmascara Errores Cuando Refactorizas Código

Supón que estás construyendo una aplicación web en la que los usuarios pueden seleccionar algún tipo de elemento. Uno de tus componentes podría tener una llamada de retorno a onSelectItem. Escribir un tipo para un elemento parece un lío, así que utilizas any como sustituto:

interface ComponentProps {
  onSelectItem: (item: any) => void;
}

Aquí tienes el código que gestiona ese componente:

function renderSelector(props: ComponentProps) { /* ... */ }

let selectedId: number = 0;
function handleSelectItem(item: any) {
  selectedId = item.id;
}

renderSelector({onSelectItem: handleSelectItem});

Más tarde retocas el selector de forma que sea más difícil pasar todo el objeto item a onSelectItem. Pero no es gran cosa, ya que sólo necesitas el ID. Cambias la firma en ComponentProps:

interface ComponentProps {
  onSelectItem: (id: number) => void;
}

Actualizas el componente y todo pasa el comprobador de tipos. ¡Victoria!

...¿o no? handleSelectItem toma un parámetro any, por lo que se contenta tanto con un elemento como con un ID. Produce una excepción en tiempo de ejecución, a pesar de pasar el comprobador de tipos. Si hubieras utilizado un tipo más específico, el comprobador de tipos lo habría detectado.

Cualquiera Oculta Tu Diseño Tipográfico

La definición del tipo para objetos complejos, como el estado de tu aplicación, puede llegar a ser bastante larga. En lugar de escribir tipos para las docenas de propiedades del estado de tu aplicación, puedes tener la tentación de utilizar un tipo any y ya está.

Esto es problemático por todas las razones enumeradas en este artículo. Pero también es problemático porque oculta el diseño de tu estado. Como explica el Capítulo 4, un buen diseño de tipos es esencial para escribir código limpio, correcto y comprensible. Con un tipo any, tu diseño de tipos es implícito. Esto hace que sea difícil saber si el diseño es bueno, o incluso cuál es el diseño. Si pides a un compañero que revise un cambio, tendrá que reconstruir si has cambiado el estado de la aplicación y cómo lo has hecho. Es mejor escribirlo para que todos lo vean.

cualquier socava la confianza en el sistema de tipos

Cada vez que en cometes un error y el comprobador de tipos lo detecta, aumenta tu confianza en el sistema de tipos. Pero cuando ves un error de tipo en tiempo de ejecución que TypeScript no ha detectado, esa confianza sufre un golpe. Si estás introduciendo TypeScript en un equipo más grande, esto puede hacer que tus compañeros se cuestionen si TypeScript merece la pena. any tipos son a menudo la fuente de estos errores no detectados.

TypeScript pretende hacerte la vida más fácil, pero TypeScript con montones de tipos any puede ser más difícil de trabajar que JavaScript sin tipos, porque tienes que corregir errores de tipo y seguir manteniendo la pista de los tipos reales en tu cabeza. Cuando tus tipos coinciden con la realidad, te liberas de la carga de tener que mantener información sobre tipos en tu cabeza. TypeScript lo hará por ti.

Para las ocasiones en que debas utilizar any, hay formas mejores y peores de hacerlo. Para saber más sobre cómo limitar los inconvenientes de any, consulta el Capítulo 5.

Cosas para recordar

  • El tipo any de TypeScript te permite desactivar la mayoría de las formas de comprobación de tipo para un símbolo.

  • El tipo any elimina la seguridad de tipos, te permite romper contratos, perjudica la experiencia del desarrollador, hace que la refactorización sea propensa a errores, oculta tu diseño de tipos y socava la confianza en el sistema de tipos.

  • Evita utilizar any cuando ¡puedas!

1 Puede que te encuentres con .tsx, .jsx, .mts, .mjs y algunas otras extensiones. Todos ellos son archivos TypeScript y JavaScript.

2 La mejor forma de intuirlo es utilizando el campo de juego de TypeScript, que muestra tu TypeScript y el JavaScript resultante uno al lado del otro.

Get TypeScript Eficaz, 2ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.