Interfacce

Edoardo Midali
Edoardo Midali

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.