Typ przecięcia, zapisywany jako A & B, opisuje wartość, która jest jednocześnie A i B. Dla typów obiektowych oznacza to, że wartość ma wszystkie właściwości obu. Tak łączy się istniejące typy bez ponownego wypisywania właściwości.
Drugiemu obiektowi brakuje team, więc to błąd kompilacji TS2322, a kolejna linia komunikatu mówi Property 'team' is missing ... but required in type 'Employee'. Wartość Staff można przekazać wszędzie tam, gdzie oczekiwany jest Person albo Employee.
Łączenie typów obiektowych
& działa z dowolną mieszanką aliasów typów, interfejsów i typów obiektowych zapisanych w miejscu, a także z generycznymi parametrami typu. W tym ostatnim przypadku trudno go czymś zastąpić: funkcja, która dodaje właściwości do dowolnego otrzymanego obiektu, może to dokładnie wyrazić.
Wywołujący zachowuje dokładny typ tego, co przekazał (title, words), plus dwie dodane właściwości. interface ... extends nie wyrazi „czymkolwiek jest T, plus to”, bo interfejs nie może rozszerzać parametru typu.
Sprzeczne właściwości stają się never
Gdy obie strony deklarują tę samą właściwość, jej typ jest przecięciem obu typów. Jeśli te typy nie mają żadnej wspólnej wartości, właściwość staje się never, a kompilator milczy, dopóki nie spróbujesz utworzyć wartości:
index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.
Błąd wskazuje na obiekt, a nie na typ, który go spowodował, więc takie bugi trudno namierzyć. Najechanie kursorem na r.id w edytorze pokazuje jego typ: never. Gdy sprzeczna właściwość jest literałowym tagiem, jak w type Shape = { kind: "circle" } & { kind: "square" }, TypeScript idzie dalej i redukuje całe przecięcie do never. Odczyt właściwości takiej wartości wyjaśnia wtedy dlaczego: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.
interface ... extends wyłapuje ten sam konflikt już przy deklaracji, błędem TS2430. To główna praktyczna różnica między nimi; strona o interface vs type porównuje je obok siebie.
Zgodne nakładanie się zawęża właściwość
Jeśli typy właściwości się nakładają, wynikiem jest ich część wspólna. To przydatne, a nie błąd:
Druga połowa pokazuje, co & robi z uniami: zostawia elementy wspólne dla obu stron. Myślenie o typach jak o zbiorach wartości sprawia, że jest to przewidywalne. A | B to suma obu zbiorów, A & B to ich część wspólna, a pusta część wspólna to never.
Przecięcie kontra unia
Nazwy pochodzą z teorii zbiorów i przy właściwościach obiektów wydają się odwrócone:
A | B (unia) | A & B (przecięcie) | |
|---|---|---|
| Wartość jest | A albo B | A i B |
| Zbiór dozwolonych wartości | większy | mniejszy |
| Właściwości, których możesz użyć | tylko te, które są w obu | wszystkie z każdego |
string z number | string | number | never |
"a" | "b" z "b" | "c" | "a" | "b" | "c" | "b" |
Przecięcie typów obiektowych ma więcej właściwości właśnie dlatego, że dopuszcza mniej wartości: tylko obiekty, które mają wszystko.
Przecięcie kontra extends
type C = A & B | interface C extends A, B | |
|---|---|---|
| Działa z | dowolnymi typami, także uniami i parametrami typu | typami obiektowymi o statycznie znanych składowych |
| Sprzeczna właściwość | po cichu staje się never | błąd TS2430 albo TS2320 przy deklaracji |
| Wynik | przecięcie sprawdzane składnik po składniku | jeden płaski nazwany typ, którego relacje są cache'owane |
| Duże złożenia | mogą spowalniać sprawdzanie typów | zalecane przez stronę Performance w wiki TypeScript |
Do połączenia kilku aliasów typów obiektowych & jest idiomatyczne i w porządku. Dla typu złożonego z wielu części albo typu publicznego API extends daje wcześniejsze błędy i tańsze sprawdzanie typów.
Przecięcia z prymitywami: branding
Przecięcie prymitywu z typem obiektowym nie daje never: string & { readonly __brand: "UserId" } to string z dodatkowym znacznikiem istniejącym tylko w czasie kompilacji. Żaden prawdziwy string nie ma tej właściwości i właśnie o to chodzi: utworzyć taką wartość może tylko kod, który celowo potwierdza brand, więc zwykłego string ani OrderId nie da się już przekazać tam, gdzie oczekiwany jest UserId. Ta technika ma własną stronę, branded types.
Najczęściej zadawane pytania
Czym jest typ przecięcia w TypeScript?
To typ zapisany jako A & B, którego wartości muszą jednocześnie spełniać A i B. Dla typów obiektowych oznacza to, że wartość ma wszystkie właściwości A i wszystkie właściwości B. To typowy sposób na połączenie dwóch aliasów typów w jeden.
Czym różni się unia od przecięcia?
Unia A | B oznacza "jedno z dwóch": wartość może być A albo B, a dopóki jej nie zawęzisz, możesz używać tylko tego, co mają wspólne. Przecięcie A & B oznacza "oba naraz": wartość ma wszystko z obu. Przy typach obiektowych unia przyjmuje więcej wartości, a przecięcie ma więcej właściwości.
Dlaczego mój typ przecięcia to never?
Bo żadna wartość nie może spełnić obu stron. string & number to never, a { id: string } & { id: number } sprawia, że id jest typu string & number, więc właściwość jest never i nie da się utworzyć żadnego obiektu. Jeśli dwa typy obiektowe mają ten sam literałowy tag z różnymi wartościami (kind: "circle" i kind: "square"), całe przecięcie redukuje się do never.
Używać przecięcia czy extends?
Oba łączą typy obiektowe. interface X extends A, B zgłasza sprzeczne właściwości przy deklaracji i to je zespół TypeScript zaleca do składania dużych typów obiektowych. & działa z każdym typem, także z uniami i parametrami generycznymi, których extends nie połączy. Używaj & dla aliasów typów i generycznych funkcji pomocniczych, a extends przy budowaniu interfejsów.
Jak połączyć dwa typy obiektowe w TypeScript?
Napisz type Merged = A & B. Dla wartości w czasie działania rozwiń oba obiekty: const merged: A & B = { ...a, ...b }. Jeśli A i B mają wspólną właściwość o różnych typach, ta właściwość dostaje typ never; użyj Omit<A, keyof B> & B, gdy właściwości drugiego obiektu mają zastąpić właściwości pierwszego.