Fundamentos de Typescript
¿Qué es Typescript?
Typescript es un superconjunto de Javascript que añade tipado estático a este lenguaje de programación. Fue creado por Microsoft y es un lenguaje de código abierto.
Video #01 👇🏽👇🏽👇🏽
Video #02 👇🏽👇🏽👇🏽 (ejercicio práctico frontend ecommerce)
¿Por qué se dice que TypeScript transpila?
El proceso de TypeScript toma código escrito en TypeScript y lo convierte a JavaScript, un lenguaje que es en esencia el mismo lenguaje de propósito general pero sin las características adicionales que ofrece TypeScript (como tipos estáticos, interfaces, etc.).
No se produce un código de nivel más bajo ni se ejecutan optimizaciones a nivel de máquina, por lo que técnicamente es una transpilación.
TIP
Sin embargo, en la práctica y en la terminología común, muchas personas se refieren al proceso de TypeScript como "compilación" porque el proceso se asemeja a la idea de tomar un código fuente y producir un archivo resultante listo para ejecutar.
compilar
Definición: Compilar generalmente se refiere a convertir código fuente de un lenguaje de alto nivel a un lenguaje de nivel más bajo, como código máquina (binario) o bytecode.
Este proceso suele involucrar una mayor transformación y optimización del código.
- Ejemplo: Compilar C++ a código máquina que pueda ser ejecutado directamente por el hardware de la computadora, o compilar Java a bytecode para la JVM.
Objetivo: Producir un código ejecutable que pueda ser interpretado por un procesador o un entorno específico, generalmente con un enfoque en el rendimiento.
¿Por qué usar Typescript?
- Tipado estático: Permite detectar errores en tiempo de compilación.
- Intellisense: Proporciona información sobre los métodos y propiedades de los objetos.
- Compatibilidad: Typescript es un superconjunto de Javascript, por lo que todo el código Javascript es válido en Typescript.
- Mantenimiento: Facilita el mantenimiento de código a medida que el proyecto crece.
- Documentación: Mejora la documentación del código.
- Comunidad: Cuenta con una gran comunidad de desarrolladores.
Extensiones VSCode
- ESLint: Linter para Javascript y Typescript.
- Prettier: Formateador de código.
- Error Lens: Muestra mensajes de error y advertencia en línea.
- JSON to TS: Convierte JSON a interfaces de Typescript.
- Pretty TypeScript Erros: Mejora la visualización de errores de Typescript.
Vite.js y Typescript
Vite.js es un entorno de desarrollo para aplicaciones web modernas que permite utilizar Typescript de forma nativa.
npm create vite@latest
tsconfig.json
El archivo tsconfig.json
es el archivo de configuración de Typescript. En él se pueden configurar diferentes opciones del compilador de Typescript.
{
"compilerOptions": {
"target": "ES2020", // Especifica la versión de JavaScript a la cual quieres que TypeScript transpile tu código, por ejemplo: ES5, ES6, ES2020, ESNext.
"useDefineForClassFields": true, // Usa la palabra clave `define` para inicializar propiedades de clase, lo que refleja más de cerca la semántica de ESNext.
"module": "ESNext", // Define el sistema de módulos que se utilizará en el código transpilado. ESNext usa la sintaxis moderna de import/export.
"lib": ["ES2020", "DOM", "DOM.Iterable"], // Especifica las bibliotecas estándar que TypeScript incluye en el entorno, como ES2020, DOM para APIs del navegador, y DOM.Iterable para estructuras iterables en el DOM.
"skipLibCheck": true, // Omite la verificación de tipos en archivos de declaración de bibliotecas (`.d.ts`), lo que puede acelerar la compilación.
/* Bundler mode */
"moduleResolution": "bundler", // Configura cómo se resuelven los módulos, adaptado para funcionar bien con herramientas de empaquetado como Webpack o Rollup.
"allowImportingTsExtensions": true, // Permite importar archivos TypeScript con extensiones explícitas, como `.ts` o `.tsx`.
"isolatedModules": true, // Garantiza que cada archivo se trate como un módulo independiente, útil para compatibilidad con herramientas de construcción.
"moduleDetection": "force", // Fuerza el tratamiento de todos los archivos como módulos, independientemente de si contienen import/export.
"noEmit": true, // Evita la emisión de archivos de salida, solo realiza la verificación de tipos.
/* Linting */
"strict": true, // Activa la verificación estricta de tipos en todo el proyecto para mayor seguridad y evitar errores comunes.
"noUnusedLocals": true, // Genera errores si hay variables locales declaradas pero no utilizadas.
"noUnusedParameters": true, // Genera errores si hay parámetros de funciones que no se utilizan.
"noFallthroughCasesInSwitch": true // Genera errores si hay casos en una declaración `switch` que pasan al siguiente caso sin un `break`.
},
"include": ["src"] // Especifica los archivos y carpetas que deben incluirse en la compilación; en este caso, todo lo que esté dentro de la carpeta `src`.
}
Conceptos claves
Vamos a repasar algunos conceptos claves de Typescript.
Datos primitivos
Typescript soporta los mismos tipos primitivos que Javascript, como number
, string
, boolean
, null
, undefined
, symbol
, y bigint
.
const age: number = 25;
const name: string = "John Doe";
const isStudent: boolean = true;
const x: null = null;
const y: undefined = undefined;
const sym: symbol = Symbol("key");
const big: bigint = 100n;
Arrays
Los arrays en Typescript pueden ser de un tipo específico o de varios tipos.
const numbers: number[] = [1, 2, 3, 4, 5];
const mixed: (string | number)[] = ["John Doe", 25, "Jane Doe", 30];
Objetos
Los objetos en Typescript pueden tener propiedades con tipos específicos.
const person: { name: string; age: number } = {
name: "John Doe",
age: 25,
};
Funciones
Las funciones en Typescript pueden tener parámetros con tipos específicos y un tipo de retorno.
function greet(name: string): string {
return `Hello, ${name}!`;
}
const greeting: string = greet("John Doe");
// arrow function
const sum = (a: number, b: number): number => a + b;
const result: number = sum(10, 20);
Interfaces
Las interfaces en Typescript permiten definir la forma de un objeto.
interface Person {
name: string;
age: number;
}
const person: Person = {
name: "John Doe",
age: 25,
};
Ejemplo #01: Contador
<body>
<main>
<h1>Counter: <span id="counter">0</span></h1>
<button id="btnCounterIncrement">Increment</button>
</main>
<script
type="module"
src="/src/main.ts"
></script>
</body>
import "./style.css";
const $counter = document.getElementById("counter") as HTMLSpanElement;
const $btnCounterIncrement = document.getElementById(
"btnCounterIncrement"
) as HTMLButtonElement;
let counter = 0;
const incrementCounter = () => {
counter += 1;
if ($counter) {
$counter.textContent = counter.toString();
}
};
if ($btnCounterIncrement) {
$btnCounterIncrement.addEventListener("click", incrementCounter);
}
En TypeScript, la palabra clave "as" se utiliza para realizar una type assertion (o "aseveración de tipo"). Esto le indica al compilador que trate una variable o expresión como si fuera de un tipo específico, incluso si TypeScript no puede inferirlo directamente.
¿Por qué es necesario utilizar as?
Tipo más específico: getElementById retorna un tipo HTMLElement | null. Sin la aserción de tipo (as), TypeScript consideraría que $counter y $btnCounterIncrement son de tipo HTMLElement | null, lo cual no te permitiría acceder directamente a las propiedades específicas de un HTMLSpanElement o HTMLButtonElement sin quejas del compilador.
Evitar errores: Al usar as, te aseguras de que TypeScript permita acceder a propiedades y métodos específicos de HTMLSpanElement y HTMLButtonElement, como textContent o addEventListener.
¿Es necesario utilizar as?
Sí si estás seguro del tipo del elemento y necesitas acceder a propiedades específicas del tipo. Sin as, TypeScript no te permitiría, por ejemplo, asignar un valor a textContent sin primero realizar una comprobación o casting adicional.
No es necesario si primero verificas el tipo de elemento con un condicional o utilizas TypeScript para inferir automáticamente el tipo en ciertos contextos. Sin embargo, hacerlo sin as requeriría más verificaciones y podría hacer el código más verboso.
Alternativa sin as:
const $counter = document.getElementById("counter");
if ($counter instanceof HTMLSpanElement) {
$counter.textContent = counter.toString();
}
¿Dónde están definidas estas interfaces?
Estas interfaces están definidas en archivos .d.ts (archivos de declaración de tipos) que forman parte de las bibliotecas estándar que TypeScript incluye cuando se configura el compilador con ciertas opciones, como "lib": ["DOM"] en tu archivo tsconfig.json.
Documentación MDN HTMLSpanElement
Ejemplo #02: Listas
<ul id="frutList"></ul>
const frutList = ["Apple", "Banana", "Orange", "Strawberry"];
const $frutList = document.getElementById("frutList") as HTMLUListElement;
frutList.forEach((frut) => {
const $frutItem = document.createElement("li");
$frutItem.textContent = frut;
$frutList.appendChild($frutItem);
});
¿Qué pasaría si la lista de frutas es un array de objetos?
const frutList = [
{ name: "Apple", color: "red" },
{ name: "Banana", color: "yellow" },
{ name: "Grape", color: "purple" },
];
const $frutList = document.getElementById("frutList") as HTMLUListElement;
frutList.forEach((frut) => {
const $frutItem = document.createElement("li");
$frutItem.textContent = `
${frut.name} - ${frut.color}
`;
$frutList.appendChild($frutItem);
});
¿Cómo se puede mejorar la legibilidad del código?
interface Frut {
name: string;
color: string;
}
const frutList: Frut[] = [
{ name: "Apple", color: "red" },
{ name: "Banana", color: "yellow" },
{ name: "Grape", color: "purple" },
];
const $frutList = document.getElementById("frutList") as HTMLUListElement;
frutList.forEach((frut) => {
const $frutItem = document.createElement("li");
$frutItem.textContent = `
${frut.name} - ${frut.color}
`;
$frutList.appendChild($frutItem);
});
¿Qué ventajas tiene utilizar interfaces?
- Legibilidad: Las interfaces permiten definir la forma de un objeto de forma clara y concisa, lo que facilita la comprensión del código.
- Mantenimiento: Las interfaces permiten reutilizar tipos en diferentes partes del código, lo que facilita el mantenimiento y la refactorización.
- Documentación: Las interfaces sirven como documentación del código, proporcionando información sobre la forma de los objetos y sus propiedades.
- Seguridad: Las interfaces permiten detectar errores en tiempo de compilación, evitando errores comunes al manipular objetos.
Ejemplo #03: Formulario para agregar Frutas
<form id="formFrut">
<input
type="text"
placeholder="frut name"
id="frutName"
/>
<input
type="text"
placeholder="frut color"
id="frutColor"
/>
<button type="submit">Add Frut</button>
</form>
<ul id="frutList"></ul>
interface Frut {
name: string;
color: string;
}
const frutList: Frut[] = [
{ name: "Apple", color: "red" },
{ name: "Banana", color: "yellow" },
{ name: "Grape", color: "purple" },
];
const $frutList = document.getElementById("frutList") as HTMLUListElement;
const $formFrut = document.getElementById("formFrut") as HTMLFormElement;
const $frutName = document.getElementById("frutName") as HTMLInputElement;
const $frutColor = document.getElementById("frutColor") as HTMLInputElement;
$formFrut.addEventListener("submit", (event) => {
event.preventDefault();
const frut: Frut = {
name: $frutName.value,
color: $frutColor.value,
};
frutList.push(frut);
$formFrut.reset();
renderFrutList();
});
const renderFrutList = () => {
$frutList.innerHTML = "";
frutList.forEach((frut) => {
const $frutItem = document.createElement("li");
$frutItem.textContent = `
${frut.name} - ${frut.color}
`;
$frutList.appendChild($frutItem);
});
};
renderFrutList();
Ejemplo #04: Frutas enum
Un enum es un tipo de datos que permite definir un conjunto de constantes con nombres simbólicos.
Para iterar sobre los valores de un enum, se puede utilizar el método Object.values(). Esto devuelve un array con los valores del enum, que se pueden recorrer con forEach().
<form id="formFrut">
<input
type="text"
placeholder="frut name"
id="frutName"
/>
<select id="frutColor">
<!-- dinamic -->
</select>
<button type="submit">Add Frut</button>
</form>
<ul id="frutList"></ul>
enum FrutColor {
Red = "red",
Yellow = "yellow",
Purple = "purple",
}
interface Frut {
name: string;
color: FrutColor;
}
const frutList: Frut[] = [
{ name: "Apple", color: FrutColor.Red },
{ name: "Banana", color: FrutColor.Yellow },
{ name: "Grape", color: FrutColor.Purple },
];
const $frutList = document.getElementById("frutList") as HTMLUListElement;
const $formFrut = document.getElementById("formFrut") as HTMLFormElement;
const $frutName = document.getElementById("frutName") as HTMLInputElement;
const $frutColor = document.getElementById("frutColor") as HTMLSelectElement;
// Iterar sobre los valores del enum y agregar opciones al select
Object.values(FrutColor).forEach((color) => {
const $option = document.createElement("option");
$option.value = color;
$option.textContent = color;
$frutColor.appendChild($option);
});
$formFrut.addEventListener("submit", (event) => {
event.preventDefault();
const frut: Frut = {
name: $frutName.value,
color: $frutColor.value as FrutColor,
};
frutList.push(frut);
$formFrut.reset();
renderFrutList();
});
const renderFrutList = () => {
$frutList.innerHTML = "";
frutList.forEach((frut) => {
const $frutItem = document.createElement("li");
$frutItem.textContent = `
${frut.name} - ${frut.color}
`;
$frutList.appendChild($frutItem);
});
};
renderFrutList();
Ejemplo #05: Consumir API
<ul id="postList"></ul>
interface User {
id: number;
name: string;
username: string;
email: string;
address: Address;
phone: string;
website: string;
company: Company;
}
interface Company {
name: string;
catchPhrase: string;
bs: string;
}
interface Address {
street: string;
suite: string;
city: string;
zipcode: string;
geo: Geo;
}
interface Geo {
lat: string;
lng: string;
}
const $userList = document.getElementById("userList") as HTMLUListElement;
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((users: User[]) => {
users.forEach((user) => {
const $userItem = document.createElement("li");
$userItem.textContent = `
${user.name} - ${user.username}
`;
$userList.appendChild($userItem);
});
});
Interfaces vs Types
Las interfaces y los tipos son dos formas de definir tipos en Typescript, pero tienen algunas diferencias clave.
Interfaces
- Definición: Las interfaces permiten definir la forma de un objeto, incluyendo las propiedades y métodos que debe tener.
- Extensión: Las interfaces pueden extender otras interfaces, permitiendo reutilizar y combinar definiciones de tipos.
- Compatibilidad: Las interfaces son más adecuadas para definir la forma de objetos y estructuras de datos.
- Extensibilidad: Las interfaces son más adecuadas para definir contratos y definiciones de tipos que pueden ser implementados por diferentes objetos.
Types
- Definición: Los tipos permiten definir tipos de datos primitivos, tipos de unión, tipos literales, y otros tipos de datos.
- Unión: Los tipos permiten definir tipos de unión, intersección, literales, y otros tipos de datos más complejos.
- Compatibilidad: Los tipos son más adecuados para definir tipos de datos primitivos y tipos de unión.
- Extensibilidad: Los tipos son más adecuados para definir tipos de datos primitivos y tipos de unión.
¿Cuándo usar interfaces y cuándo usar types?
- Interfaces: Utiliza interfaces cuando necesites definir la forma de un objeto o estructura de datos, o cuando necesites extender o combinar definiciones de tipos.
- Types: Utiliza types cuando necesites definir tipos de datos primitivos, tipos de unión, tipos literales, o tipos más complejos que no requieran la definición de una estructura de datos.
// Definir una interfaz para un objeto de usuario
interface User {
id: number;
name: string;
}
// Extender una interfaz
interface Admin extends User {
adminLevel: string;
}
// Implementar una interfaz en una clase
class Employee implements User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
// Definir un alias de tipo para un objeto
type User = {
id: number;
name: string;
};
// Crear un alias de tipo complejo
type Address = {
street: string;
city: string;
};
// Definir una intersección de tipos
type UserWithAddress = User & Address;
// Definir una unión de tipos
type ID = number | string;
const userId: ID = "123abc";
// Definir un tipo literal
type Direction = "left" | "right" | "up" | "down";
const direction: Direction = "left";