Generics Interfacce

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.