Generics Interfacce

Edoardo Midali
Edoardo Midali

Le interfacce generiche in TypeScript permettono di definire contratti e strutture dati riutilizzabili che operano su tipi parametrici. Dichiarate con parametri tipo <T>, le interfacce generiche specificano forme e comportamenti che possono essere applicati a tipi diversi mantenendo type safety, abilitando astrazione di pattern comuni e garantendo coerenza attraverso implementazioni multiple.

Fondamenti e Sintassi

Un’interfaccia generica si dichiara aggiungendo parametri tipo tra angle brackets dopo il nome dell’interfaccia.

// Interfaccia generica base
interface Box<T> {
  contenuto: T;
  svuota(): void;
  riempi(valore: T): void;
}

// Implementazione con tipo specifico
const boxNumero: Box<number> = {
  contenuto: 42,
  svuota() {
    this.contenuto = 0;
  },
  riempi(valore: number) {
    this.contenuto = valore;
  },
};

const boxStringa: Box<string> = {
  contenuto: "hello",
  svuota() {
    this.contenuto = "";
  },
  riempi(valore: string) {
    this.contenuto = valore;
  },
};

// Interfaccia per oggetti con chiave-valore
interface KeyValue<K, V> {
  chiave: K;
  valore: V;
}

const config: KeyValue<string, number> = {
  chiave: "porta",
  valore: 3000,
};

Interfacce per Funzioni Generiche

Definire firme di funzioni generiche attraverso interfacce.

// Interfaccia funzione generica
interface Trasformatore<T, U> {
  (input: T): U;
}

const toStringa: Trasformatore<number, string> = (n) => n.toString();
const lunghezza: Trasformatore<string, number> = (s) => s.length;

// Interfaccia con metodi generici
interface Processore<T> {
  elabora(item: T): T;
  valida(item: T): boolean;
  trasforma<U>(item: T, fn: (val: T) => U): U;
}

const processoreNumeri: Processore<number> = {
  elabora(item) {
    return item * 2;
  },
  valida(item) {
    return item > 0;
  },
  trasforma(item, fn) {
    return fn(item);
  },
};

// Interfaccia con callable e proprietà
interface Comparatore<T> {
  (a: T, b: T): number;
  reverse?: boolean;
}

const confrontaNumeri: Comparatore<number> = (a, b) => a - b;
confrontaNumeri.reverse = false;

Multipli Parametri Tipo

Le interfacce possono avere multipli parametri tipo per relazioni complesse.

// Due parametri tipo
interface Coppia<T, U> {
  primo: T;
  secondo: U;
  swap(): Coppia<U, T>;
}

const coppia: Coppia<string, number> = {
  primo: "età",
  secondo: 30,
  swap() {
    return {
      primo: this.secondo,
      secondo: this.primo,
      swap() {
        return coppia;
      },
    };
  },
};

// Tre parametri tipo
interface Tripletta<T, U, V> {
  a: T;
  b: U;
  c: V;
}

// Mappa generica
interface Map<K, V> {
  get(chiave: K): V | undefined;
  set(chiave: K, valore: V): void;
  has(chiave: K): boolean;
  delete(chiave: K): boolean;
  clear(): void;
  size(): number;
}

Constraints sui Parametri Tipo

Applicare vincoli ai parametri tipo nelle interfacce per limitare i tipi accettabili.

// Constraint con extends
interface Repository<T extends { id: number }> {
  items: T[];
  aggiungi(item: T): void;
  rimuovi(id: number): void;
  trova(id: number): T | undefined;
  getTutti(): T[];
}

interface Utente {
  id: number;
  nome: string;
  email: string;
}

const repoUtenti: Repository<Utente> = {
  items: [],
  aggiungi(utente) {
    this.items.push(utente);
  },
  rimuovi(id) {
    this.items = this.items.filter((u) => u.id !== id);
  },
  trova(id) {
    return this.items.find((u) => u.id === id);
  },
  getTutti() {
    return [...this.items];
  },
};

// Constraint con keyof
interface Selettore<T, K extends keyof T> {
  oggetto: T;
  chiave: K;
  valore: T[K];
}

interface Persona {
  nome: string;
  eta: number;
}

const selettore: Selettore<Persona, "nome"> = {
  oggetto: { nome: "Mario", eta: 30 },
  chiave: "nome",
  valore: "Mario",
};

// Multiple constraints
interface Confrontabile<T extends { compare(other: T): number }> {
  min(items: T[]): T | undefined;
  max(items: T[]): T | undefined;
  sort(items: T[]): T[];
}

Interfacce per Data Structures

Definire interfacce per strutture dati comuni con generics.

// Stack generico
interface Stack<T> {
  push(item: T): void;
  pop(): T | undefined;
  peek(): T | undefined;
  isEmpty(): boolean;
  size(): number;
}

// Queue generico
interface Queue<T> {
  enqueue(item: T): void;
  dequeue(): T | undefined;
  front(): T | undefined;
  isEmpty(): boolean;
  size(): number;
}

// LinkedList generico
interface Node<T> {
  valore: T;
  next: Node<T> | null;
}

interface LinkedList<T> {
  head: Node<T> | null;
  aggiungi(valore: T): void;
  rimuovi(valore: T): boolean;
  trova(valore: T): Node<T> | null;
  toArray(): T[];
}

// Tree generico
interface TreeNode<T> {
  valore: T;
  figli: TreeNode<T>[];
  parent: TreeNode<T> | null;
}

interface Tree<T> {
  root: TreeNode<T> | null;
  inserisci(valore: T, parent?: TreeNode<T>): TreeNode<T>;
  cerca(valore: T): TreeNode<T> | null;
  attraversa(callback: (nodo: TreeNode<T>) => void): void;
}

Interfacce con Readonly

Definire interfacce immutabili con proprietà readonly.

// Interfaccia readonly
interface ImmutableBox<T> {
  readonly contenuto: T;
  readonly createdAt: Date;
}

const box: ImmutableBox<number> = {
  contenuto: 42,
  createdAt: new Date(),
};

// box.contenuto = 10; // ERRORE: readonly

// Readonly su proprietà specifiche
interface Config<T> {
  readonly tipo: string;
  valore: T;
  modificabile: boolean;
}

// Readonly con array
interface Lista<T> {
  readonly items: readonly T[];
  aggiungi(item: T): Lista<T>;
}

const lista: Lista<number> = {
  items: [1, 2, 3],
  aggiungi(item) {
    return {
      items: [...this.items, item],
      aggiungi: this.aggiungi,
    };
  },
};

Interfacce con Index Signatures

Combinare generics con index signatures per oggetti dinamici.

// Index signature generica
interface Dictionary<T> {
  [key: string]: T;
}

const numeri: Dictionary<number> = {
  uno: 1,
  due: 2,
  tre: 3,
};

const stringhe: Dictionary<string> = {
  nome: "Mario",
  cognome: "Rossi",
};

// Index signature con metodi
interface Cache<T> {
  [key: string]: T;
  get(key: string): T | undefined;
  set(key: string, value: T): void;
  has(key: string): boolean;
  clear(): void;
}

// Record type (più type-safe)
interface Mappatura<K extends string | number, V> {
  mappa: Record<K, V>;
  aggiungi(chiave: K, valore: V): void;
  rimuovi(chiave: K): void;
}

Default Type Parameters

Specificare tipi default per parametri tipo nelle interfacce.

// Default type
interface Risposta<T = any> {
  success: boolean;
  data: T;
  messaggio?: string;
}

// Usa default (any)
const risposta1: Risposta = {
  success: true,
  data: "qualsiasi cosa",
};

// Specifica tipo esplicitamente
const risposta2: Risposta<number> = {
  success: true,
  data: 42,
};

// Multipli default
interface Config<T = string, U = number> {
  nome: T;
  valore: U;
}

const config1: Config = { nome: "porta", valore: 3000 };
const config2: Config<symbol, boolean> = { nome: Symbol(), valore: true };

// Default condizionale
interface Container<T, Required extends boolean = false> {
  valore: Required extends true ? T : T | undefined;
}

Interfacce che Estendono Interfacce Generiche

Ereditarietà tra interfacce generiche per composizione di contratti.

// Estensione base
interface Entita<T> {
  id: T;
  creatoIl: Date;
}

interface EntitaModificabile<T> extends Entita<T> {
  modificatoIl: Date;
  modificaDa: string;
}

interface Utente extends EntitaModificabile<number> {
  nome: string;
  email: string;
}

// Estensione con parametri tipo aggiuntivi
interface Collezione<T> {
  items: T[];
  count(): number;
}

interface CollezioneOrdinabile<T, K extends keyof T> extends Collezione<T> {
  ordina(chiave: K): void;
  ordinaInverso(chiave: K): void;
}

// Estensione con constraint
interface Base<T> {
  valore: T;
}

interface Estesa<T extends object> extends Base<T> {
  merge(altro: T): T;
  chiavi(): Array<keyof T>;
}

Interfacce per Pattern

Definire interfacce per pattern architetturali comuni.

// Observable pattern
interface Observer<T> {
  update(data: T): void;
}

interface Observable<T> {
  observers: Observer<T>[];
  subscribe(observer: Observer<T>): () => void;
  unsubscribe(observer: Observer<T>): void;
  notify(data: T): void;
}

// Builder pattern
interface Builder<T> {
  build(): T;
}

interface UserBuilder extends Builder<User> {
  setNome(nome: string): this;
  setEmail(email: string): this;
  setEta(eta: number): this;
}

// Repository pattern
interface IRepository<T, ID> {
  findById(id: ID): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  update(id: ID, entity: Partial<T>): Promise<T>;
  delete(id: ID): Promise<boolean>;
}

// Strategy pattern
interface Strategy<T, R> {
  execute(input: T): R;
}

interface Context<T, R> {
  strategy: Strategy<T, R>;
  setStrategy(strategy: Strategy<T, R>): void;
  executeStrategy(input: T): R;
}

Interfacce per API Response

Modellare risposte API con interfacce generiche type-safe.

// Response generica
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
  timestamp: number;
}

// Paginazione
interface Paginated<T> {
  items: T[];
  page: number;
  pageSize: number;
  totalItems: number;
  totalPages: number;
}

// Result type
interface Result<T, E = Error> {
  isSuccess(): this is Success<T>;
  isError(): this is Failure<E>;
  value?: T;
  error?: E;
}

interface Success<T> extends Result<T, never> {
  value: T;
}

interface Failure<E> extends Result<never, E> {
  error: E;
}

// Fetch result
interface FetchResult<T> {
  data?: T;
  loading: boolean;
  error?: string;
  refetch(): Promise<void>;
}

Interfacce per Validazione

Definire contratti per validazione type-safe.

// Validatore generico
interface Validator<T> {
  validate(value: T): ValidationResult;
}

interface ValidationResult {
  isValid: boolean;
  errors: string[];
}

// Schema validation
interface Schema<T> {
  validate(data: unknown): data is T;
  parse(data: unknown): T;
  safeParse(data: unknown): Result<T, ValidationError[]>;
}

interface ValidationError {
  path: string[];
  message: string;
  code: string;
}

// Form validation
interface FormField<T> {
  value: T;
  error?: string;
  touched: boolean;
  validate(): boolean;
}

interface Form<T extends Record<string, any>> {
  fields: { [K in keyof T]: FormField<T[K]> };
  isValid(): boolean;
  getValues(): T;
  reset(): void;
}

Interfacce con Conditional Types

Usare conditional types nelle interfacce per comportamenti dinamici.

// Conditional property
interface Wrapper<T, Required extends boolean = false> {
  value: Required extends true ? T : T | undefined;
  isRequired: Required;
}

const required: Wrapper<string, true> = {
  value: "hello", // deve essere string
  isRequired: true,
};

const optional: Wrapper<string, false> = {
  value: undefined, // può essere undefined
  isRequired: false,
};

// Conditional methods
interface Collection<T, Mutable extends boolean = true> {
  items: T[];
  get(index: number): T | undefined;
  add: Mutable extends true ? (item: T) => void : never;
  remove: Mutable extends true ? (index: number) => void : never;
}

// Readonly vs mutable
type ReadonlyOrMutable<T, R extends boolean> = R extends true ? Readonly<T> : T;

interface State<T, R extends boolean = false> {
  data: ReadonlyOrMutable<T, R>;
  update: R extends false ? (newData: T) => void : never;
}

Interfacce Ricorsive

Definire interfacce che si riferiscono a se stesse.

// Tree ricorsivo
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

// JSON value
interface JsonValue {
  [key: string]: string | number | boolean | null | JsonValue | JsonValue[];
}

// Nested object
interface NestedObject<T> {
  value: T;
  nested?: NestedObject<T>;
}

// Menu ricorsivo
interface MenuItem {
  label: string;
  url?: string;
  children?: MenuItem[];
}

Utility Interfaces

Interfacce generiche utility per trasformazioni comuni.

// DeepPartial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface DeepPartialable<T> {
  merge(partial: DeepPartial<T>): T;
}

// DeepReadonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface Freezable<T> {
  freeze(): DeepReadonly<T>;
}

// Nullable/Optional
interface Nullable<T> {
  value: T | null;
  hasValue(): this is { value: T };
}

interface Optional<T> {
  value: T | undefined;
  hasValue(): this is { value: T };
  orElse(defaultValue: T): T;
}

Conclusioni

Le interfacce generiche in TypeScript forniscono meccanismo potente per definire contratti riutilizzabili e type-safe che operano su tipi parametrici. Combinando parametri tipo, constraints, default types, ed ereditarietà, è possibile modellare strutture dati complesse, pattern architetturali, e API contracts mantenendo type safety completa. Le interfacce generiche eliminano duplicazione nella definizione di tipi, migliorano manutenibilità attraverso astrazione, e permettono al compilatore di verificare correttezza delle implementazioni, risultando essenziali per librerie, framework, e applicazioni che richiedono contratti flessibili su tipi diversi con garanzie statiche.