TypeScript ha un ricco sistema di tipi, ma non è necessario comprenderlo tutto per essere produttivi. La realtà è che il 90% del lavoro viene svolto da una manciata di funzionalità a cui si ricorre ogni giorno. Questo è il riferimento pratico — primitivi, union, generici, tipi di utilità e i pattern di narrowing che fanno clic su tutto. Nessuna teoria astratta; ogni esempio è il tipo di codice che scrivi in un progetto reale. Se conosci bene JavaScript e hai fatto un po' di TypeScript ma vuoi consolidare il tuo modello mentale, questo fa per te. La storia completa è nel Manuale di TypeScript — questo articolo è la versione 80/20.
Primitivi e Letterali
I tipi primitivi di TypeScript corrispondono direttamente ai tipi a runtime di JavaScript:
string, number, boolean, null,
undefined, bigint e symbol. Userai i primi
tre costantemente; gli altri compaiono in contesti specifici.
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null; // nullable pattern
let refreshToken: string | undefined; // optional patternDove TypeScript diventa interessante sono i tipi letterali. Invece di dire solo
che un valore è una string, puoi dire che deve essere uno di un insieme specifico di stringhe.
Questo è molto più utile di una semplice annotazione string perché TypeScript può
rilevare i valori non validi in fase di compilazione:
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;
function navigate(dir: Direction) {
console.log(`Moving ${dir}`);
}
navigate('north'); // ✅
navigate('up'); // ❌ Argument of type '"up"' is not assignable to type 'Direction'
// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});(string & {}) alla fine mantiene l'autocompletamento funzionante per i
valori noti accettando comunque qualsiasi stringa. È un pattern comune nelle librerie di design system.interface vs type — Quando Usare Quale
Questa è la domanda che ogni principiante di TypeScript si pone. La risposta pratica: usa
interface per le forme degli oggetti — specialmente quelle che rappresentano contratti API pubblici o
che altri tipi estenderanno. Usa type per union, intersezioni, tipi mappati
e qualsiasi cosa che non sia puramente una forma di oggetto. In pratica sono in gran parte intercambiabili per
le forme degli oggetti — scegli una convenzione e mantienila coerente all'interno di una codebase.
// interface — object shapes, extensible via extends
interface User {
id: number;
email: string;
createdAt: Date;
}
interface AdminUser extends User {
role: 'admin';
permissions: string[];
}
// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;
// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
createdAt: Date;
updatedAt: Date;
};
type UserRecord = User & WithTimestamps;
// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string; code: number };Una differenza significativa: le interfacce supportano la fusione delle dichiarazioni —
puoi dichiarare la stessa interfaccia due volte e TypeScript unisce le definizioni. È così che
le librerie aumentano i tipi globali (es. aggiungendo proprietà a Window). I tipi non
si fondono; ridichiarare un tipo è un errore.
Tipi Union e Intersezione
I tipi union (A | B) dicono "questo valore è A o B". I tipi intersezione
(A & B) dicono "questo valore è sia A che B allo stesso tempo". Le union sono
ovunque nel codice reale — il pattern più potente che abilitano è la
union discriminata, che è come modelli i dati che possono essere in stati diversi
senza ricorrere a campi opzionali ovunque.
// Discriminated union — model API response states cleanly
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string; retryable: boolean };
// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
switch (state.status) {
case 'idle': return '<p>Not started</p>';
case 'loading': return '<p>Loading...</p>';
case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
case 'error': return `<p>Error: ${state.error}</p>`;
}
}
// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
label: string;
disabled?: boolean;
};
type IconButtonProps = ButtonBaseProps & {
icon: string;
iconPosition: 'left' | 'right';
};Il pattern della union discriminata si affida a un discriminante — una proprietà letterale
(qui status) unica per ogni variante. TypeScript usa quella
proprietà per restringere il tipo completo all'interno dei rami condizionali, dandoti accesso type-safe ai
campi specifici della variante come data o error.
Generici — La Parte che Fa Inciampare Tutti
Il problema motivante: vuoi una funzione che funzioni su più tipi, ma usare
any butta via tutte le informazioni sul tipo. I generici risolvono questo — sono
parametri di tipo, scritti tra parentesi angolari, che permettono a una funzione o interfaccia di funzionare su una
famiglia di tipi mantenendo la piena sicurezza dei tipi.
// Without generics — any loses all information
function first(arr: any[]): any {
return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here
// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅La sintassi <T> dichiara un parametro di tipo. Puoi chiamarlo come preferisci
— T è solo la convenzione per un singolo tipo generico. Quando chiami la funzione,
TypeScript inferisce T dagli argomenti, quindi raramente devi scriverlo esplicitamente.
Le interfacce generiche sono altrettanto utili per modellare le forme delle API:
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
data: T;
meta: {
page: number;
totalPages: number;
totalItems: number;
};
}
interface User {
id: number;
name: string;
email: string;
}
// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const res = await fetch('/api/users');
return res.json();
}
const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number
// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
const user = findById(users, 42); // T inferred as Useras per ottenere il
tipo di ritorno che vuoi. Questi sono i due segnali che un generico sarebbe più pulito.Tipi di Utilità che Userai Davvero
TypeScript include un insieme di tipi di utilità incorporati che trasformano i tipi esistenti in nuovi. Eliminano la necessità di duplicare manualmente o modificare le definizioni di tipo. Ecco quelli che compaiono costantemente nelle codebase reali:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }
async function updateUser(id: number, payload: Partial<User>) {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(payload)
});
}
// Required<T> — all properties become required (opposite of Partial)
interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }
// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields
// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses
// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property
// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };
// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;
// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
return {
token: crypto.randomUUID(),
userId,
expiresAt: new Date(Date.now() + 86_400_000)
};
}
type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in syncunknown vs any vs never
Questi tre tipi confondono la maggior parte degli sviluppatori finché non capiscono quale problema ciascuno
risolve. any è la via d'uscita — disabilita completamente il controllo dei tipi per quel
valore. È utile quando si migra JavaScript in TypeScript, ma l'uso eccessivo vanifica lo scopo di
TypeScript. unknown è l'alternativa type-safe: il valore potrebbe essere qualsiasi cosa,
ma devi restringerlo prima di poterci fare qualcosa.
never è un tipo che non può mai verificarsi — il fondo della gerarchia dei tipi.
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}
// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
if (typeof input === 'string') {
return input.toUpperCase(); // ✅ narrowed to string inside this block
}
if (typeof input === 'number') {
return input.toFixed(2); // ✅ narrowed to number
}
throw new Error(`Cannot transform value of type ${typeof input}`);
}
// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
if (
typeof raw === 'object' &&
raw !== null &&
'apiUrl' in raw &&
typeof (raw as any).apiUrl === 'string'
) {
return raw as ResolvedConfig;
}
throw new Error('Invalid config format');
}
// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default:
// If you add a new Shape variant and forget to handle it here,
// TypeScript will error: Type 'NewShape' is not assignable to type 'never'
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}Narrowing dei Tipi nella Pratica
Il narrowing dei tipi è il modo in cui TypeScript affina un tipo ampio (string | number,
unknown, una union discriminata) a un tipo specifico all'interno di un blocco condizionale.
La
documentazione sul narrowing di TypeScript
copre ogni guardia in profondità — ecco i pattern che scriverai quotidianamente.
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
if (typeof val === 'string') return val.trim();
if (typeof val === 'number') return val.toLocaleString();
return val ? 'Yes' : 'No';
// TypeScript knows val must be boolean here
}
// instanceof — class/object narrowing
function handleError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'An unknown error occurred';
}
// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };
function makeNoise(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow(); // TypeScript narrows to Cat
} else {
animal.bark(); // TypeScript narrows to Dog
}
}
// Discriminated union narrowing (most common in component state)
type LoadState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function renderState<T>(state: LoadState<T>) {
if (state.status === 'success') {
console.log(state.data); // ✅ data is accessible
} else if (state.status === 'error') {
console.error(state.message); // ✅ message is accessible
}
}
// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
if (isUser(payload)) {
console.log(payload.email); // ✅ fully typed
}
}L'operatore typeof
è lo strumento di narrowing più fondamentale. Combinato con
instanceof, l'operatore in e i controlli delle proprietà discriminanti,
puoi gestire praticamente qualsiasi scenario di narrowing senza ricorrere a any o cast non sicuri.
Conclusione
Il sistema dei tipi di TypeScript premia l'apprendimento dei fondamentali piuttosto che la memorizzazione
di funzionalità oscure. I tipi letterali e le union discriminate eliminano intere categorie di bug a runtime.
I generici ti permettono di scrivere astrazioni riutilizzabili e type-safe. I tipi di utilità come
Partial, Pick, Omit e ReturnType mantengono
i tuoi tipi DRY. E unknown con il narrowing ti offre la sicurezza dei tipi anche ai
bordi dove i dati arrivano dal mondo esterno. Il modo migliore per consolidare tutto questo è
sperimentare interattivamente — il
TypeScript Playground
ti permette di incollare qualsiasi snippet e vedere immediatamente i tipi inferiti, senza configurazione.
Quando lavori con dati JSON in progetti TypeScript, gli strumenti
JSON Formatter e
JSON to TypeScript su questo sito possono generare
automaticamente definizioni di interfaccia da payload API reali.