Partial<T> to wbudowany typ narzędziowy, który czyni każdą właściwość T opcjonalną. To naturalny typ dla aktualizacji lub łatki: wywołujący wysyła tylko pola, które się zmieniły.
Partial<User> to { id?: number; name?: string; email?: string }. Kompilator nadal sprawdza pola, które przekazujesz: { nmae: "x" } czy { name: 42 } to błąd, i właśnie dlatego Partial jest lepszy niż luźny parametr object lub any.
Jak zdefiniowany jest Partial
Partial to jednolinijkowy typ mapowany w bibliotece standardowej TypeScriptu:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Dla każdego klucza P z T deklaruje opcjonalną właściwość o tym samym typie. Ponieważ mapuje po keyof T, zachowuje readonly na właściwościach, które je miały. Jako zwykły typ nie ma wpływu na działanie programu: obiekt przekazany jako changes jest w obu przypadkach tym samym obiektem.
Odczyt z Partial daje T | undefined
Każdej właściwości Partial<T> może brakować, więc jej odczyt daje typ właściwości plus undefined. Kompilator każe ci obsłużyć przypadek braku:
{ ...defaults, ...opts } to typowy sposób, żeby zamienić Partial<Options> z powrotem w kompletne Options: najpierw rozłóż wartości domyślne, a podane wartości niech je nadpiszą.
Pułapka jawnego undefined
Właściwości opcjonalnej może brakować, ale może też istnieć z wartością undefined. Rozkładanie obiektu kopiuje to undefined na prawdziwą wartość, a typ wyniku tego nie pokazuje:
To gryzie, gdy łatka powstaje z formularza lub query stringa, w którym puste pola stają się undefined. Opcja kompilatora exactOptionalPropertyTypes (nie jest częścią strict) sprawia, że { name: undefined } to błąd kompilacji dla name?: string, chyba że napiszesz name?: string | undefined, co zapobiega problemowi u źródła.
Partial jest płytki
Partial czyni opcjonalnymi tylko właściwości najwyższego poziomu. Zagnieżdżony obiekt, jeśli go przekazujesz, musi być kompletny:
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
Właściwość editor jest opcjonalna, ale gdy już jest, ma typ { fontSize: number; tabSize: number }, bez zmian. Przy obiektach ustawień, łatkach API i danych testowych często chcesz właściwości opcjonalnych na każdym poziomie. To wymaga typu rekurencyjnego.
DeepPartial: rekurencyjny Partial
TypeScript nie ma wbudowanej głębokiej wersji, ale to kilka linii. Funkcje i tablice zostają bez zmian, bo opcjonalne elementy tablicy pozwoliłyby na [undefined]:
Typ jest rekurencyjny, scalanie już nie. applySettings scala editor ręcznie, bo rozkładanie obiektu też jest płytkie. Generyczna funkcja głębokiego scalania istnieje w bibliotekach takich jak lodash (merge), a jej typowanie jest trudniejsze niż powyższy typ.
Required: przeciwieństwo Partial
Required<T> usuwa ? z każdej właściwości. Jest zdefiniowany z modyfikatorem -?, który usuwa też undefined z typu każdej właściwości:
Wzorzec jest taki sam jak przy Partial, tylko odwrócony: użytkownicy API przekazują luźną konfigurację, a kod w środku pracuje na wersji Required, w której każda wartość na pewno istnieje. Rozkładanie ma tę samą dziurę co funkcja aktualizująca wyżej: wywołujący, który jawnie przekaże port: undefined, nadpisze wartość domyślną przez undefined, a kompilator to zaakceptuje, chyba że exactOptionalPropertyTypes jest włączone. Required jest płytki tak samo jak Partial.
Tylko niektóre właściwości opcjonalne lub wymagane
Partial i Required działają na każdą właściwość. Aby zmienić tylko kilka, podziel typ przez Pick i Omit i złóż go z powrotem:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| Typ | Efekt | Głęboko? |
|---|---|---|
Partial<T> | każda właściwość opcjonalna | nie |
Required<T> | każda właściwość wymagana, undefined usunięte | nie |
DeepPartial<T> (własny) | opcjonalne na każdym poziomie | tak |
PartialBy<T, K> (własny) | opcjonalne tylko klucze K | nie |
Readonly<T> | każda właściwość readonly | nie |
Najczęściej zadawane pytania
Co robi Partial w TypeScript?
Partial<T> tworzy typ ze wszystkimi właściwościami T oznaczonymi jako opcjonalne. Dla interface User { name: string; email: string } Partial<User> to { name?: string; email?: string }, więc {}, { name: "Ada" } i pełny użytkownik to poprawne wartości.
Czy Partial w TypeScript działa głęboko?
Nie, Partial dotyczy tylko właściwości najwyższego poziomu. Zagnieżdżony obiekt wewnątrz Partial<T> nadal musi być kompletny. Dla wersji rekurencyjnej napisz typ DeepPartial<T>, który stosuje się sam do siebie na właściwościach o typie obiektowym.
Co jest przeciwieństwem Partial w TypeScript?
Required<T>. Usuwa ? z każdej właściwości i usuwa też undefined z ich typów, więc Required<{ port?: number }> to { port: number }. Jest zdefiniowany jako typ mapowany z modyfikatorem -?.
Jak uczynić opcjonalnymi tylko niektóre właściwości?
Połącz Omit, Pick i Partial: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. PartialBy<User, "email"> zostawia każdą właściwość bez zmian poza email, które staje się opcjonalne.
Dlaczego właściwość jest undefined po scaleniu aktualizacji Partial?
Partial<T> pozwala, by właściwość istniała z wartością undefined, a rozkładanie obiektu ją kopiuje: { ...user, ...{ name: undefined } } ma name: undefined, choć TypeScript typuje wynik jako User. Odfiltruj wartości undefined przed scaleniem albo włącz exactOptionalPropertyTypes, żeby jawne undefined było odrzucane.