TypeScript
TypeScript es un lenguaje de programación de código abierto desarrollado y mantenido por Microsoft. Es un superconjunto de JavaScript, que esencialmente añade tipado estático y objetos basados en clases.
También es un compilador que traduce código TypeScript a JavaScript. El código TypeScript se compila a JavaScript, que es el código que se ejecuta en el navegador.
Podemos pensar que Typescript como un linter, que nos ayuda a detectar errores en tiempo de compilación.
Ventajas de TypeScript
- Tipado estático. TypeScript añade tipado estático a JavaScript. El tipado estático permite detectar ciertas categorías de errores en tiempo de compilación. Por ejemplo, intentar asignar un
string
a una variable de tiponumber
dará un error en tiempo de compilación.
function sum(a: number, b: number) {
return a + b;
}
sum(1, 2); // 3
sum("1", "2"); // Error: Argument of type '"1"' is not assignable to parameter of type 'number'.
function sum(a: number, b: number) {
return a + b;
}
sum(1, 2); // 3
sum("1", "2"); // Error: Argument of type '"1"' is not assignable to parameter of type 'number'.
Al tipar las variables, podemos saber los métodos y propiedades. De esta manera el IDE puede ayudarnos a programar.
Desventajas de TypeScript
Curva de aprendizaje. TypeScript añade una capa de complejidad a JavaScript. Aunque la sintaxis es muy similar, hay que aprender a usar los tipos, interfaces, clases, etc.
Requiere compilación. TypeScript no se puede ejecutar directamente en el navegador. Primero hay que compilarlo a JavaScript. Esto añade un paso más al flujo de trabajo.
No se ejecutan las validaciones en runtime. Las validaciones de tipos se hacen en tiempo de compilación. En tiempo de ejecución no hay validaciones de tipos.
Visual Studio Code
Visual Studio Code es un editor de código fuente desarrollado por Microsoft para Windows, Linux y macOS.
Este editor por defecto, aunque estemos trabajando en un archivo .js
, utiliza el motor de TypeScript para mostarnos los tooltips, mientras estamos programando.
Instalación
Para instalar TypeScript, necesitamos tener instalado Node.js.
npm install -g typescript
npm install -g typescript
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist", // Carpeta donde se compilará el código
"target": "ES6", // Versión de JavaScript a la que se compilará. Muy parecido a Babel
"declaration": true, // Genera los archivos de definición de tipos
"allowJs": true, // Permite convivir con JavaScript
"lib": ["DOM", "DOM.Iterable", "ESNext"] // Librerías que queremos que estén disponibles
},
"include": ["src"] // Carpeta donde se encuentra el código fuente
}
{
"compilerOptions": {
"outDir": "./dist", // Carpeta donde se compilará el código
"target": "ES6", // Versión de JavaScript a la que se compilará. Muy parecido a Babel
"declaration": true, // Genera los archivos de definición de tipos
"allowJs": true, // Permite convivir con JavaScript
"lib": ["DOM", "DOM.Iterable", "ESNext"] // Librerías que queremos que estén disponibles
},
"include": ["src"] // Carpeta donde se encuentra el código fuente
}
Compilación
tsc
tsc
El compilador de TS, tsc, no minifica el código... Para ello podemos usar Webpack.
En Webpack, podemos usar el plugin ts-loader
para que compile TypeScript.
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: "ts-loader",
options: {
transpileOnly: true, // Que al momento de hacer la build, no compruebe los tipos. Eso es útil cuando estamos pasando codigo de JS a TS
},
},
}
]
}
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: "ts-loader",
options: {
transpileOnly: true, // Que al momento de hacer la build, no compruebe los tipos. Eso es útil cuando estamos pasando codigo de JS a TS
},
},
}
]
}
Tipos de datos
Boolean
let isDone: boolean = false;
let isDone: boolean = false;
Number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
let color: string = "blue";
color = "red";
let color: string = "blue";
color = "red";
Array
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
Destructuring
const myCar = [2002, "Ford", "Focus"];
const [year, make, model] = myCar;
const myCar = [2002, "Ford", "Focus"];
const [year, make, model] = myCar;
El problema es que cada valor destructurado, lo entiende como string o number.
Por lo que tenemos que hacer un casting.
const myCar = [2002, "Ford", "Focus"];
const [year, make, model] = myCar as [number, string, string];
const myCar = [2002, "Ford", "Focus"];
const [year, make, model] = myCar as [number, string, string];
O bien podemos hacer un type alias.
type Car = [number, string, string];
const myCar = [2002, "Ford", "Focus"] as Car;
const [year, make, model] = myCar;
type Car = [number, string, string];
const myCar = [2002, "Ford", "Focus"] as Car;
const [year, make, model] = myCar;
Tuple
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error
Readonly
let x: readonly [string, number] = ["hello", 10];
x[0] = "world"; // Error
let x: readonly [string, number] = ["hello", 10];
x[0] = "world"; // Error
Enum
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
Any
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
Void
function warnUser(): void {
console.log("This is my warning message");
}
function warnUser(): void {
console.log("This is my warning message");
}
Null and Undefined
let u: undefined = undefined;
let n: null = null;
let u: undefined = undefined;
let n: null = null;
Never
function error(message: string): never {
throw new Error(message);
}
function error(message: string): never {
throw new Error(message);
}
Object
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
Index Signatures
const x: { [key: string]: number } = {};
x.a = 1;
x["b"] = 2;
const x: { [key: string]: number } = {};
x.a = 1;
x["b"] = 2;
En objetos, podemos usar index signatures para definir propiedades dinámicas.
interface Person {
name: string;
age: number;
[key: string]: any;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30, address: "Main Street" });
interface Person {
name: string;
age: number;
[key: string]: any;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30, address: "Main Street" });
En objetos complejos.
const phones: { [key: string]: { brand: string; model: string } } = {
iPhone: { brand: "Apple", model: "iPhone 12" },
Galaxy: { brand: "Samsung", model: "Galaxy S21" },
Pixel: { brand: "Google", model: "Pixel 5" },
};
const phones: { [key: string]: { brand: string; model: string } } = {
iPhone: { brand: "Apple", model: "iPhone 12" },
Galaxy: { brand: "Samsung", model: "Galaxy S21" },
Pixel: { brand: "Google", model: "Pixel 5" },
};
Cuando hacemos key: string, estamos diciendo que la key puede ser cualquier string. Pero podemos ser más específicos. Ya que, si no acepta cualquier string, aunque no exista en el objeto, no dará error.
phones.iPhone; // { brand: 'Apple', model: 'iPhone 12' }
phones.android; // undefined, pero no da error
phones.iPhone; // { brand: 'Apple', model: 'iPhone 12' }
phones.android; // undefined, pero no da error
const phones: {
[key in "iPhone" | "Galaxy" | "Pixel"]: { brand: string; model: string };
} = {
iPhone: { brand: "Apple", model: "iPhone 12" },
Galaxy: { brand: "Samsung", model: "Galaxy S21" },
Pixel: { brand: "Google", model: "Pixel 5" },
};
phones.iPhone; // { brand: 'Apple', model: 'iPhone 12' }
phones.android; // Error
const phones: {
[key in "iPhone" | "Galaxy" | "Pixel"]: { brand: string; model: string };
} = {
iPhone: { brand: "Apple", model: "iPhone 12" },
Galaxy: { brand: "Samsung", model: "Galaxy S21" },
Pixel: { brand: "Google", model: "Pixel 5" },
};
phones.iPhone; // { brand: 'Apple', model: 'iPhone 12' }
phones.android; // Error
Type assertions
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
Static Typing
El motor se encarga de comprobar en tiempo de desarrollo que los tipos de datos son correctos.
function sum(a: number, b: number) {
return a + b;
}
sum(1, 2); // 3
sum("1", "2"); // Error: Argument of type '"1"' is not assignable to parameter of type 'number'.
function sum(a: number, b: number) {
return a + b;
}
sum(1, 2); // 3
sum("1", "2"); // Error: Argument of type '"1"' is not assignable to parameter of type 'number'.
Inferencia de tipos
TypeScript le asigna un tipo a cada variable, aunque no lo especifiquemos. Inferencia de tipos.
let a = 1; // number
let b = "1"; // string
let c = true; // boolean
let a = 1; // number
let b = "1"; // string
let c = true; // boolean
Tipos literales
let humidity = 79 as 79; // 79
let humidity = 79 as const; // 79
let humidity = 79 as 79; // 79
let humidity = 79 as const; // 79
Any y type casting
let a: any = 1;
let b = a as number;
let c = <number>a;
let a: any = 1;
let b = a as number;
let c = <number>a;
Intersections
En TypeScript, podemos hacer uniones de tipos con el operador &
.
interface A {
a: number;
}
interface B {
b: number;
}
type C = A & B;
const c: C = { a: 1, b: 2 };
// const c: C = { a: 1 }; // Error
interface A {
a: number;
}
interface B {
b: number;
}
type C = A & B;
const c: C = { a: 1, b: 2 };
// const c: C = { a: 1 }; // Error
Interface y Type
La diferencia entre interface y type
Las interfaces son más flexibles y se pueden extender.
interface A {
a: number;
}
interface B {
b: number;
}
interface C extends A, B {
c: number;
}
interface A {
a: number;
}
interface B {
b: number;
}
interface C extends A, B {
c: number;
}
Los interfaces solo sirven para definir la forma de un objeto. Por lo que cuando vamos a definir un tipo primitivo, siempre vamos a usar type
.
Con los type
podemos hacer uniones, intersecciones, etc.
type A = number | string;
type B = A & { b: number };
type C = A | { c: number };
type A = number | string;
type B = A & { b: number };
type C = A | { c: number };
Las interfaces se pueden declarar varias veces, y se van a ir extendiendo. Los type
no.
interface A {
a: number;
}
interface A {
b: number;
}
const a: A = { a: 1, b: 2 };
type B = { a: number };
type B = { b: number }; // Error
interface A {
a: number;
}
interface A {
b: number;
}
const a: A = { a: 1, b: 2 };
type B = { a: number };
type B = { b: number }; // Error
Interfaces
Las interfaces son una forma poderosa de definir contratos en TypeScript. Las interfaces son un tipo de dato que define una estructura de datos.
interface Person {
name: string;
age: number;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30 });
interface Person {
name: string;
age: number;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30 });
Propiedades opcionales
interface Person {
name: string;
age?: number;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John" });
interface Person {
name: string;
age?: number;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John" });
Anidadas
interface User {
name: string;
address: {
street: string;
city: string;
};
}
interface UserResponse {
success: boolean;
user: User;
}
function printUser(data: UserResponse) {
console.log(data.user.name, data.user.address.city, data.success);
}
printUser({
success: true,
user: { name: "John", address: { street: "Main Street", city: "New York" } },
});
interface User {
name: string;
address: {
street: string;
city: string;
};
}
interface UserResponse {
success: boolean;
user: User;
}
function printUser(data: UserResponse) {
console.log(data.user.name, data.user.address.city, data.success);
}
printUser({
success: true,
user: { name: "John", address: { street: "Main Street", city: "New York" } },
});
Index Signatures
interface Person {
name: string;
age: number;
[key: string]: any;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30, address: "Main Street" });
interface Person {
name: string;
age: number;
[key: string]: any;
}
function printPerson(person: Person) {
console.log(person.name, person.age);
}
printPerson({ name: "John", age: 30, address: "Main Street" });
Extends
interface A {
a: number;
}
interface B {
b: number;
}
interface C extends A, B {
c: number;
}
const c: C = { a: 1, b: 2, c: 3 };
interface A {
a: number;
}
interface B {
b: number;
}
interface C extends A, B {
c: number;
}
const c: C = { a: 1, b: 2, c: 3 };
Implements
La diferencia entre extends
e implements
es que extends
se usa para extender una interface, y implements
se usa para implementar una interfaz.
interface A {
a: number;
}
interface B {
b: number;
}
class C implements A, B {
a = 1;
b = 2;
}
interface A {
a: number;
}
interface B {
b: number;
}
class C implements A, B {
a = 1;
b = 2;
}
Open interfaces
Las interfaces son abiertas, por lo que se pueden declarar varias veces, y se van a ir extendiendo.
interface A {
a: number;
}
interface A {
b: number;
}
const a: A = { a: 1, b: 2 };
interface A {
a: number;
}
interface A {
b: number;
}
const a: A = { a: 1, b: 2 };
Global
Podemos declarar interfaces globales, y luego extenderlas.
declare global {
interface Window {
myVar: string;
}
}
window.myVar = "Hello";
declare global {
interface Window {
myVar: string;
}
}
window.myVar = "Hello";
Type
Puede contener cualquier tipo de dato, incluyendo primitivos, objetos, funciones, etc. Se puede usar para definir tipos primitivos, tipos de unión, tipos de intersección, etc.
type A = number | string;
type B = A & { b: number };
type C = A | { c: number };
type A = number | string;
type B = A & { b: number };
type C = A | { c: number };
Implements
type A = {
a: number;
};
type B = {
b: number;
};
class C implements A, B {
a = 1;
b = 2;
}
type A = {
a: number;
};
type B = {
b: number;
};
class C implements A, B {
a = 1;
b = 2;
}
Aunque no es recomendable, ya que los type
pueden ser tipos primitivos, y no tienen sentido implementarlos. En este caso es mejor usar para los implementos las interfaces
.
type A = number;
type B = {
b: number;
};
class C implements A, B {
// Error
a = 1;
b = 2;
}
type A = number;
type B = {
b: number;
};
class C implements A, B {
// Error
a = 1;
b = 2;
}
Recursión
Podemos encontrar recursión simple.
type NestedNumbers = number | NestedNumbers[];
const numbers: NestedNumbers = [1, [2, [3, 4], 5], 6];
type NestedNumbers = number | NestedNumbers[];
const numbers: NestedNumbers = [1, [2, [3, 4], 5], 6];
En este caso estamos definiendo un tipo que puede ser un número o un array de números, y a su vez, cada número puede ser un array de números.