Series: TypeScript
Introduzione Link to heading
Rinfreschiamoci la memoria: che cosa è l’optional chaining? L’optional chaining prende forma nell’operatore ?.
, l’elvis operator per gli amici, e ci permettere di leggere il valore di una proprietà in profondità in una chain di oggetti senza preoccuparci che ogni singola reference sia valida:
const customer = {
name: "Carl",
details: {
age: 82,
location: "Paradise Falls"
}
};
const customerCity = customer.details?.address?.city; // undefined
Il valore di ripiego, nel caso in cui la chain fallisca, è sempre undefined
. Qualcosa di simile esiste anche nel type system, e in questo articolo andiamo a vedere di che si tratta.
Il problema Link to heading
Ipotizziamo di trovarci nella seguente situazione:
interface Endpoint<Request, Response> {
request: Request;
response: Response;
}
interface User {
name: string;
age: number;
id: string;
}
interface UserAPI {
"/users": {
get: Endpoint<null, User[]>;
post: Endpoint<Omit<User, "id">, User>;
};
"/users/:userId": {
get: Endpoint<null, User>;
patch: Endpoint<Partial<Omit<User, "id">>, User>;
};
}
L’interfaccia UserAPI
contiene, per ogni endpoint, i tipi delle richieste e delle risposte per i metodi HTTP
che uno specifico endpoint supporta.
Ipotizziamo ora di voler tipizzare il tipo di ritorno della seguente funzione:
declare function extractPostRequest<Path extends keyof UserAPI>(api: UserAPI, path: Path): unknown
la quale, data una istanza di UserAPI
e un path
chiave di UserAPI
, estrae la request del metodo POST
, a patto che tale metodo venga supportato dall’endpoint corrispondente al path
.
Potremmo essere tentati di fare così:
declare function extractPostRequest<Path extends keyof UserAPI>(api: UserAPI, path: Path): UserAPI[Path]["post"]["request"]
ma TypeScript ci ferma: "post"
non appartiene alle chiavi di UserAPI[Path]
dato che vi sono alcuni Path
non contenenti alcuna chiave "post"
.
La soluzione Link to heading
OptionalLookup Link to heading
type OptionalLookup<T, K extends PropertyKey> = T[K & keyof T]
Vi sono innanzitutto alcune proprietà importanti da tenere presenti:
- per ogni tipo
T
diverso daany
si ha cheT[never] = never
- per ogni tipo
K
assegnabile astring | number | symbol
si ha chenever[K] = never
- per ogni tripla di tipi
A
,B
eC
si ha che(A | B) & C = (A & C) | (B & C)
, ovvero l’intersezione si distribuisce rispetto all’unione - per ogni coppia di tipi
A
eB
si ha che se non esistono valori assegnabili sia adA
che aB
alloraA & B = never
- per ogni tipo
A
si ha cheA | never = A
Analizziamo ora la semantica della type function OptionalLookup
. Per le proprietà 3
, 4
e 5
se la generica chiave K
è assegnabile a keyof T
allora K & keyof T
si riduce semplicemente a K
, altrimenti si riduce a never
. Perciò nel primo caso avremmo che OptionalLookup<T, K> = T[K]
, cioè si comporta come il normale lookup, mentre nel secondo caso avremmo che OptionalLookup<T, K> = T[never] = never
grazie alla proprietà 1
. Infine, per la proprietà 2
abbiamo che OptionalLookup
può essere innestato a piacimento anche nel caso in cui il lookup fallisca nel tipo never
:
type obj = {
prop1: {
innerProp1: number,
innerProp2: string,
},
prop2: boolean[]
}
type test1 = OptionalLookup<obj, "prop1"> // { innerProp1: number; innerProp2: string; }
type test2 = OptionalLookup<OptionalLookup<obj, "prop1">, "innerProp1"> // number
type test3 = OptionalLookup<obj, "prop3"> // never
type test4 = OptionalLookup<OptionalLookup<obj, "prop1">, "innerProp3"> // never
type test5 = OptionalLookup<OptionalLookup<obj, "prop3">, "whatever"> // never
Possiamo quindi tipizzare la funzione extractPostRequest
nel seguente modo:
declare function extractPostRequest<Path extends keyof UserAPI>(
api: UserAPI,
path: Path
): OptionalLookup<OptionalLookup<UserAPI[Path], "post">, "request">;
declare const userAPI: UserAPI
extractPostRequest(userAPI, "/users") // Omit<User, "id">
extractPostRequest(userAPI, "/users/:userId") // never
In questo modo se il path
corrisponde ad un endpoint che supporta il metodo POST
allora il tipo di ritorno sarà il tipo della request
corrispondente, altrimenti sarà never
. Una possibile implementazione di extractPostRequest
potrebbe infatti decidere di lanciare una eccezione nel caso in cui il path
non supporti il metodo POST
, e never
è il tipo di ritorno corretto da scegliere per questa evenienza.