TypeScript
- Introducció
- Des de JSDoc
- Transició des de JavaScript
- Build step
Introducció
TypeScript és un superset de JavaScript que afegeix tipat estàtic al llenguatge. Això significa que pots especificar tipus per a variables, paràmetres de funció i retorns de funció, cosa que ajuda a evitar molts errors comuns de programació.
Què afegeix TypeScript:
- Annotations de tipus: TypeScript et permet especificar tipus (per exemple, number, string, boolean, tipus personalitzats) per a variables, paràmetres i valors de retorn, cosa que fa que TypeScript pugui detectar errors en temps de compilació en lloc de temps d’execució.
let age: number = 30; // `age` can only be a number
- Interfaces i alies de tipus: Pots definir l'estructura dels objectes i especificar com han de ser les estructures de dades, fent que el codi sigui més estructurat i previsible.
interface User {
name: string;
age: number;
}
- Enums: TypeScript afegeix enums per definir constants amb nom, fent que el codi sigui més llegible i evitant valors no vàlids.
enum Direction {
North,
South,
East,
West
}
-
Classes i interfícies amb tipat estricte: TypeScript admet classes i interfícies fortament tipades, ajudant a assegurar un ús consistent de classes i objectes en tota l’aplicació.
-
Genèrics: TypeScript afegeix genèrics per escriure funcions i classes reutilitzables i tipus segurs.
function wrapInArray<T>(value: T): T[] {
return [value];
}
Aquests són els avantatges per al desenvolupador:
-
Detecció primerenca d'errors: TypeScript detecta errors durant el desenvolupament, cosa que ajuda a reduir errors i problemes en temps d'execució. Això és especialment útil en projectes més grans.
-
Millora en llegibilitat i mantenibilitat: Amb anotacions de tipus, els desenvolupadors poden entendre amb més facilitat la funció i els valors esperats de variables i funcions, fins i tot quan revisiten el codi després de molt temps.
-
Millor autocompleció i eines: Editors com VSCode poden proporcionar millor autocompleció, suport per a la refactorització i eines de depuració, tot impulsat pel sistema de tipus de TypeScript.
-
Escalabilitat: L’estructura i la seguretat de tipus de TypeScript el fan ideal per a projectes més grans i equips, on el codi ha de ser fàcilment llegible i mantenible per múltiples desenvolupadors.
-
Compatibilitat amb JavaScript: TypeScript es compila en JavaScript pur, així que es pot utilitzar en qualsevol entorn on s'executi JavaScript, incloent navegadors web i Node.js.
Aquests serien alguns inconvenients:
-
Corba d'aprenentatge: TypeScript introdueix sintaxi i conceptes addicionals, que poden requerir temps perquè els desenvolupadors de JavaScript els aprenguin i s'hi adaptin.
-
Pas Addicional de compilació: TypeScript necessita compilar-se a JavaScript, cosa que afegeix un pas de compilació que pot alentir el desenvolupament en alguns fluxos de treball.
-
Sintaxi més estricta: La rigorositat de TypeScript pot semblar pesada per a prototips ràpids o petits scripts on els beneficis del tipat rigorós poden no ser tan evidents.
-
Possibilitat de sobrecàrrega: En projectes petits o tasques de desenvolupament ràpid, l'estructura i definicions de tipus de TypeScript poden de vegades semblar una feina extra sense grans beneficis.
Des de JSDoc
TypeScript i JSDoc milloren ambdós JavaScript amb seguretat de tipus, però difereixen en l'enfocament i les capacitats. TypeScript ofereix un sistema de tipus integrat, que permet la detecció d'errors anticipada en temps de compilació, un excel·lent suport d'eines i escalabilitat per a projectes grans, tot i que requereix un pas de compilació i té una corba d'aprenentatge més pronunciada. En canvi, JSDoc permet anotacions de tipus directament a JavaScript sense compilació, fent-lo flexible i més senzill d'implementar per a projectes petits o migracions graduals, però proporciona una comprovació de tipus limitada i manca les característiques completes de TypeScript, cosa que el fa menys adequat per a codi més gran.
Quan tenim un codi amb JSDoc que volem canviar a TypeScript, començar per reanomenar tots els arxius de .js
a .ts
i treure totes les anotacions de JSDoc. Compte, els imports han d'utilitzar el nom de l'arxiu .js
.
Transició des de JavaScript
Tipus de variables
Afegeix tipus utilitzant : type
després del nom de variable:
let message: string = "Hello, world!";
let count: number = 10;
Tenim dos tipus especials: unknown
i any
.
El tipus any
permet ignorar el sistema de comprovació de tipus, ja que pots fer qualsevol operació sense que es mostrin errors. S'hauria d'evitar sempre que es pugui. Exemple:
let value: any = "Hello";
value = 42; // No error
value.toUpperCase(); // No error, even if value is not a string at runtime
El tipus unknown
és una alternativa més segura a any
. Pot contenir qualsevol tipus, però obliga a fer comprovació de tipus o assercions abans d'utilitzar-ho.
let value: unknown = "Hello";
if (typeof value === "string") {
console.log(value.toUpperCase()); // Safe to call string methods here
}
TypeScript intentarà inferir els tipus segons l'ús que es faci de les variables i les funcions. El consell és treballar amb el compilador en mode estricte (veure la configuració tsconfig.json suggerida més endavant), perquè així es queixarà si hi ha algun tipus indefinit. Alguns casos d'inferència de tipus:
let name = "Alice"; // TypeScript infers `name` as `string`
function add(a: number, b: number) {
return a + b; // TypeScript infers the return type as `number`
}
const numbers = [1, 2, 3];
numbers.forEach(num => console.log(num * 2)); // `num` is inferred as `number`
Aquests són casos on el compilador no pot inferir el tipus:
function greet(name) {
return `Hello, ${name}!`; // 'name' has an implicit 'any' type, but 'strict' mode warns
}
try {
// Some code that might throw an error
} catch (error) {
console.log(error.message); // Error: Object is of type 'unknown'
}
const person = { name: "Alice", age: 25 };
const key = "name" as string;
console.log(person[key]); // Error: expression of type 'string' can't be used to index type
A l'últim cas, la solució té a veure amb utilitzar keyof
:
type Person = { name: string; age: number };
const person: Person = { name: "Alice", age: 25 };
const key: keyof Person = "name"; // Allowed, because `key` is of type `"name" | "age"`
Funcions
Afegeix : type
darrere dels parèntesis de la funció:
function greet(name: string): string {
return `Hello, ${name}`;
}
function logMessage(message: string): void {
console.log(message);
}
Arrays i objectes
Per a arrays, especifica el tipus seguit de []
:
let numbers: number[] = [1, 2, 3];
let names: string[] = ["Alice", "Bob"];
Per a objectes, defineix l'estructura de cada propietat:
let person: { name: string; age: number } = {
name: "Alice",
age: 25
};
Tuples
Una tupla de TypeScript és un array de mida fixa on cada element pot tenir un tipus diferent:
const t1: [number, string] = [1, "Alice"];
const t2: [number, string] = [...t1]; // spreading a tuple
t2[0] = 2; t2[1] = 'Bob'; // mutating a tuple
const t3: unknown = [3, "Eve"]; // tuple without type
const t4 = t3 as [number, string]; // as a tuple
const t5 = t3 as (number | string)[]; // as an array
Interfícies i tipus
Una interfície defineix la forma d'un objecte reusable, de forma molt similar als tipus:
interface PersonI {
name: string;
age: number;
}
type PersonT = {
name: string;
age: number;
};
let person1: PersonI = { name: "Alice", age: 25 };
let person2: PersonT = { name: "Bob", age: 27 };
Les interfícies i els tipus es poden estendre:
// interfaces
interface Animal {
species: string;
}
interface Dog extends Animal {
breed: string;
}
// types
type Animal = {
species: string;
};
type Dog = Animal & {
breed: string;
};
Els tipus permeten definir primitius, unions i interseccions:
type ID = string | number; // Union type
type Coordinates = { x: number; y: number } & { z: number }; // Intersection
Les interfícies permeten la seva implementació per una classe:
interface Person {
name: string;
age: number;
greet(): void;
}
class User implements Person {
constructor(public name: string, public age: number) {}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
Tipus Union
Les tipus Union permeten una variable acceptar diversos tipus:
function display(value: string | number): void {
console.log(value);
}
Propietats i paràmetres opcionals
Les propietats i paràmetres opcionals s'indiquen amb ?
després del nom:
type User = {
name: string;
age?: number; // age is optional
};
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}`;
}
El tipus de greeting
serà string | undefined
.
Tipus de funcions
Indica en el tipus d'un callback els tipus de l'entrada i la sortida:
function process(data: string, callback: (input: string) => void): void {
callback(data);
}
function processAndReturn(data: number, callback: (input: number) => number): number {
return callback(data);
}
Pots utilitzar tipus:
type Transformer = (input: string) => string;
function transformData(data: string, transformer: Transformer): string {
return transformer(data);
}
Per a utilitzar genèrics:
function mapArray<T, U>(array: T[], callback: (item: T) => U): U[] {
return array.map(callback);
}
const words = ["hello", "world", "typescript"];
const wordLengths = mapArray(words, (item) => item.length);
console.log(wordLengths); // Output: [5, 5, 9]
Comprovacions de tipus
Per assegurar-te que una variable tingui el tipus esperat, pots utilitzar diferents tipus de type guards en TypeScript. A continuació, s'expliquen les tècniques més comunes.
Comprovació per veritat (truthy)
Quan es comprova si una variable no és nul·la ni undefined, pots refinar el tipus simplement verificant si és "truthy":
let str: string | null;
// ...
if (str) { // str no és null ni undefined aquí
console.log(str.length);
}
Tot i que pots utilitzar l'operador !
per indicar que una variable no és nul·la ni undefined
, això pot ser arriscat i cal evitar-ho sempre que sigui possible:
let value: string | null = getValueFromSomewhere();
console.log(value!.length); // Es tracta com no nul·la, però podria fallar si no ho és.
Explícit amb as
Pots informar el compilador d'un tipus específic utilitzant el type assertion as
. Això és útil, però cal fer-ho amb precaució, ja que el compilador confia completament en tu:
let value: any = "This is a string";
let length: number = (value as string).length; // Narrowing explícit
Amb typeof (només primitius)
L'operador typeof
permet verificar el tipus d'una variable per assegurar que és un dels tipus primitius com string
, number
, boolean
, etc.:
let str: unknown;
// ...
if (typeof str === 'string') { // str és un string aquí
console.log(str.length);
}
Amb instanceof (només classes)
L'operador instanceof
s'utilitza per verificar si un objecte és una instància d'una classe concreta:
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
let pet: Dog | Cat;
// ...
if (pet instanceof Dog) {
pet.bark();
} else {
pet.meow();
}
Amb in
El in
permet comprovar si una propietat existeix en un objecte. Això és útil quan treballes amb tipus d'objecte que tenen propietats exclusives:
interface Car {
drive: () => void;
}
interface Boat {
sail: () => void;
}
let vehicle: Car | Boat;
// ...
if ("drive" in vehicle) {
vehicle.drive();
} else {
vehicle.sail();
}
Amb tipus literals
Pots refinar literalment el tipus de les variables amb operadors d'igualtat (===
o !==
) per a unions de literals:
type Shape = "circle" | "square";
function drawShape(shape: Shape) {
if (shape === "circle") {
console.log("Dibuixant un cercle");
} else {
console.log("Dibuixant un quadrat");
}
}
Amb unions discriminades
Quan treballes amb unions de tipus d'objecte que tenen una propietat comuna (el discriminant), pots utilitzar aquesta propietat per refinar el tipus:
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.width * shape.height;
}
}
Amb funcions personalitzades
Pots crear els teus propis type guards amb una funció que retorni un tipus de la forma variable is Type
:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
Control de flux automàtic
TypeScript també pot refinar tipus basant-se en el control de flux sense necessitat de guards explícits. Per exemple, en verificar valors null
o undefined
:
function processValue(value: string | null | undefined) {
if (!value) {
console.log("Valor no proporcionat");
} else {
console.log("Longitud del valor:", value.length);
}
}
Build step
Per a gestionar un projecte amb TypeScript des de zero, cal preparar un package.json
mitjançant npm, instal.lar TypeScript per a desenvolupament i generar la seva configuració. Aquests serien els passos habituals:
$ npm init -y # creates an empty package.json
$ npm install typescript --save-dev # installs typescript package for development
$ npx tsc --init # inits the tsconfig.json file with compiler options
Alternativament, podem editar directament dos arxius de text amb els paràmetres necessaris:
package.json
{
"name": "your-project",
"version": "0.1.0",
"scripts": {
"compile": "tsc",
},
"devDependencies": {
"typescript": "^5.5.4"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"sourceMap": true
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules"
]
}
Això vol dir que els arxius TypeScript els trobarà a src
. La carpeta node_modules
cal ignorar-la, ja que inclou tots els packages instal.lats de les dependències.
Aquesta configuració també indica que utilitzarà el mode estricte (strict: true
). És una pràctica desitjada, ja que implica escriure codi amb bones pràctiques. Principalment, obliga a fer anotacions de tipus per a les variables i comprovacions de null per als objectes. I no infereix mai que el tipus de la variable és any
si no ha trobat una anotació.
Per a instal.lar les dependències (en aquest cas, typescript) caldrà fer:
$ npm install
Llavors podem compilar a JavaScript així:
$ npm run compile
I tots els arxius js es trobaran a dist
.
Resumint, aquesta seria l'estructura de carpetes:
/your-project
├── /dist // compiled js files
├── /node_modules // packages installed by npm
├── /src // ts files
├── package.json // npm config file
├── tsconfig.json // compiler configuration