Interfacce

Le interfacce in TypeScript definiscono contratti che descrivono la forma di oggetti, specificando quali proprietà e metodi devono essere presenti con i loro tipi. Questo meccanismo permette di dichiarare strutture riutilizzabili che garantiscono coerenza nel codice, facilitano refactoring, e abilitano type checking statico per prevenire errori a compile time.
Sintassi Base
Un’interfaccia dichiara la struttura di un oggetto con proprietà tipizzate.
// Interfaccia semplice
interface Utente {
nome: string;
eta: number;
email: string;
}
// Uso
const utente: Utente = {
nome: "Mario",
eta: 30,
email: "mario@example.com",
};
// Errore se mancano proprietà
// const invalido: Utente = { nome: "Luigi" }; // ERRORE
Proprietà Opzionali
Proprietà marcate con ? sono opzionali e possono essere omesse.
interface Config {
porta: number;
host?: string;
debug?: boolean;
}
const config1: Config = {
porta: 3000,
};
const config2: Config = {
porta: 8080,
host: "localhost",
debug: true,
};
Proprietà Readonly
Proprietà readonly possono essere assegnate solo durante inizializzazione.
interface Punto {
readonly x: number;
readonly y: number;
}
const punto: Punto = { x: 10, y: 20 };
// punto.x = 5; // ERRORE: readonly
// Readonly su array
interface Lista {
readonly items: readonly number[];
}
Metodi
Le interfacce possono dichiarare firme di metodi.
interface Calcolatrice {
somma(a: number, b: number): number;
sottrai(a: number, b: number): number;
}
const calc: Calcolatrice = {
somma(a, b) {
return a + b;
},
sottrai(a, b) {
return a - b;
},
};
// Sintassi alternativa per metodi
interface Logger {
log: (message: string) => void;
error: (error: Error) => void;
}
Index Signatures
Definire proprietà con chiavi dinamiche usando index signatures.
// Index signature string
interface Dictionary {
[key: string]: string;
}
const traduzioni: Dictionary = {
hello: "ciao",
goodbye: "arrivederci",
};
// Index signature number
interface ArrayLike {
[index: number]: string;
length: number;
}
// Mixed con proprietà note
interface Config {
name: string;
[key: string]: any;
}
Estensione di Interfacce
Le interfacce possono estendere altre interfacce ereditandone le proprietà.
// Interfaccia base
interface Entita {
id: number;
createdAt: Date;
}
// Estensione singola
interface Utente extends Entita {
nome: string;
email: string;
}
// Estensione multipla
interface Timestamp {
updatedAt: Date;
}
interface Prodotto extends Entita, Timestamp {
nome: string;
prezzo: number;
}
const prodotto: Prodotto = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
nome: "Laptop",
prezzo: 999,
};
Interfacce per Funzioni
Dichiarare tipo di funzioni attraverso interfacce callable.
// Function interface
interface Comparatore {
(a: number, b: number): number;
}
const crescente: Comparatore = (a, b) => a - b;
const decrescente: Comparatore = (a, b) => b - a;
// Con proprietà aggiuntive
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
Interfacce Generiche
Parametrizzare interfacce con type parameters per riutilizzo.
// Interfaccia generica
interface Box<T> {
contenuto: T;
apri(): T;
}
const boxNumero: Box<number> = {
contenuto: 42,
apri() {
return this.contenuto;
},
};
const boxStringa: Box<string> = {
contenuto: "hello",
apri() {
return this.contenuto;
},
};
// Multiple type parameters
interface Coppia<K, V> {
chiave: K;
valore: V;
}
const config: Coppia<string, number> = {
chiave: "porta",
valore: 3000,
};
Interfacce Ibride
Interfacce che combinano callable signature con proprietà.
interface jQuery {
(selector: string): HTMLElement[];
version: string;
ajax(url: string): Promise<any>;
}
// Implementazione esempio
const $: jQuery = Object.assign((selector: string) => [], {
version: "3.0",
ajax: (url: string) => Promise.resolve({}),
});
Declaration Merging
Multiple dichiarazioni della stessa interfaccia si fondono automaticamente.
// Prima dichiarazione
interface Utente {
nome: string;
}
// Seconda dichiarazione (merge)
interface Utente {
eta: number;
}
// Risultato: merge di entrambe
const utente: Utente = {
nome: "Mario",
eta: 30,
};
// Utile per estendere librerie esterne
declare global {
interface Window {
customProperty: string;
}
}
Interfacce vs Type Aliases
Differenze chiave tra interfacce e type aliases.
// Interfaccia
interface IUtente {
nome: string;
}
// Type alias
type TUtente = {
nome: string;
};
// Interfacce supportano declaration merging
interface IUtente {
eta: number;
}
// Type NO declaration merging
// type TUtente = { eta: number }; // ERRORE
// Type supportano unions
type StringOrNumber = string | number;
// Interfacce supportano extends più naturalmente
interface Admin extends IUtente {
permessi: string[];
}
Implementazione in Classi
Le classi implementano interfacce usando keyword implements.
interface Animale {
nome: string;
verso(): string;
}
class Cane implements Animale {
nome: string;
constructor(nome: string) {
this.nome = nome;
}
verso(): string {
return "Bau";
}
}
// Multiple interfaces
interface Identificabile {
id: number;
}
class Gatto implements Animale, Identificabile {
id: number;
nome: string;
constructor(id: number, nome: string) {
this.id = id;
this.nome = nome;
}
verso(): string {
return "Miao";
}
}
Interfacce Annidate
Definire strutture complesse con interfacce annidate.
interface Azienda {
nome: string;
indirizzo: {
via: string;
citta: string;
cap: string;
};
dipendenti: {
nome: string;
ruolo: string;
}[];
}
// Con interfacce separate (preferibile)
interface Indirizzo {
via: string;
citta: string;
cap: string;
}
interface Dipendente {
nome: string;
ruolo: string;
}
interface Azienda2 {
nome: string;
indirizzo: Indirizzo;
dipendenti: Dipendente[];
}
Utility Types con Interfacce
Applicare utility types TypeScript built-in a interfacce.
interface Utente {
id: number;
nome: string;
email: string;
password: string;
}
// Partial: tutte proprietà opzionali
type UtenteUpdate = Partial<Utente>;
// Pick: seleziona proprietà specifiche
type UtentePublic = Pick<Utente, "id" | "nome">;
// Omit: escludi proprietà specifiche
type UtenteSafe = Omit<Utente, "password">;
// Required: tutte proprietà obbligatorie
type UtenteRequired = Required<Utente>;
// Readonly: tutte proprietà readonly
type UtenteImmutabile = Readonly<Utente>;
Mapped Types da Interfacce
Creare nuove interfacce trasformando esistenti.
interface Prodotto {
nome: string;
prezzo: number;
disponibile: boolean;
}
// Tutti nullable
type NullableProdotto = {
[K in keyof Prodotto]: Prodotto[K] | null;
};
// Tutti string
type StringProdotto = {
[K in keyof Prodotto]: string;
};
// Con getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type ProdottoGetters = Getters<Prodotto>;
// { getNome: () => string; getPrezzo: () => number; ... }
Best Practices
Convenzioni e pattern per uso efficace delle interfacce.
// 1. Prefisso I opzionale (meno comune oggi)
interface IUtente {} // stile C#
interface Utente {} // preferito
// 2. Nomi descrittivi
interface UtenteRegistrato {}
interface ConfigurazioneServer {}
// 3. Separare concerns
interface UtenteDati {
nome: string;
email: string;
}
interface UtenteMetodi {
salva(): Promise<void>;
elimina(): Promise<void>;
}
// 4. Composizione
interface UtenteCompleto extends UtenteDati, UtenteMetodi {}
// 5. Readonly quando appropriato
interface ImmutableConfig {
readonly apiKey: string;
readonly endpoint: string;
}
Conclusioni
Le interfacce in TypeScript forniscono meccanismo potente per definire contratti e strutture di tipi, garantendo coerenza e type safety attraverso il codebase. Supporto per proprietà opzionali, readonly, metodi, generics, ed estensione permette di modellare strutture dati complesse mantenendo flessibilità. Declaration merging e implementazione in classi abilitano pattern architetturali avanzati, mentre integrazione con utility types e mapped types offre trasformazioni sofisticate. Le interfacce risultano essenziali per API design, dependency injection, e architetture che richiedono separazione tra contratti e implementazioni.