Menu

Interface vs type w TypeScript: różnice i kiedy czego użyć

interface i type mogą opisywać kształty obiektów i zwykle sprawdzi się każde z nich. Poznaj prawdziwe różnice: łączenie deklaracji, unie i mapped types, extends kontra przecięcia, niejawne sygnatury indeksu, komunikaty błędów i wydajność kompilatora, a do tego jasną regułę wyboru.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Przy opisywaniu kształtu obiektu interface i type robią to samo, a wartości jednego da się przypisać do drugiego, gdy kształty się zgadzają. Różnice leżą na obrzeżach: type może nazwać rzeczy, których interfejs nie nazwie (unie, krotki, typy wyliczane), a interfejs potrafi kilka rzeczy niedostępnych dla aliasu typu (łączenie, sprawdzane extends).

TypeScript porównuje typy obiektowe według struktury, więc nazwa deklaracji nie ma znaczenia dla przypisywalności. Poniżej jest lista miejsc, w których wybór naprawdę ma znaczenie.

Tabela porównawcza

Cechainterfacetype
Kształty obiektówtaktak
Pola opcjonalne, readonly, metody, sygnatury indeksutaktak
Generykitaktak
Unie (A | B)nietak
Krotki, prymitywy, typy funkcji same w sobienie (typy funkcji tylko jako sygnatury wywołania)tak
Mapped types i conditional typesnietak
Rozszerzanieextends, konflikty są błędami&, konflikty stają się never
Łączenie deklaracjitaknie (zduplikowany identyfikator)
Przypisywalne do Record<string, T>nietak, gdy właściwości pasują
implements w klasietaktak, jeśli to typ obiektowy
Definicje rekurencyjnetaktak

Co potrafi tylko type

Wszystko, co nie jest pojedynczym kształtem obiektu, wymaga aliasu typu:

Żadnego z nich nie da się zapisać przez interface (dwa ostatnie opisują strony o mapped types i conditional types). To praktyczny powód, dla którego każdy projekt w końcu gdzieś używa type, niezależnie od tego, czego używa do kształtów obiektów.

Co potrafi tylko interface: łączenie deklaracji

Dwie deklaracje interface o tej samej nazwie w tym samym zasięgu łączą się w jedną. Dwie deklaracje type o tej samej nazwie to błąd TS2300, Duplicate identifier.

interface Settings {
  theme: string;
}
interface Settings {
  fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both

type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.

Łączenie pozwala rozszerzać typy bibliotek z zewnątrz: dodać właściwość do globalnego Window, do Request z Expressa albo do typu motywu biblioteki. Jeśli publikujesz typy, które użytkownicy mogą chcieć rozszerzyć, używaj interfejsów. We własnym kodzie aplikacji przypadkowe połączenie (dwa pliki skryptów deklarujące tę samą nazwę globalnego interfejsu) dzieje się po cichu, chyba że obie deklaracje mają tę samą właściwość z różnymi typami. To jeden z argumentów, którymi niektóre zespoły uzasadniają wybór type.

extends kontra przecięcie

Interfejs rozszerza się przez extends, alias typu przez &. Zwykle dają ten sam wynik, ale inaczej traktują sprzeczną właściwość. extends zgłasza konflikt przy deklaracji:

index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
  Types of property 'id' are incompatible.
    Type 'number' is not assignable to type 'string'.

Przecięcie przyjmuje ten sam konflikt po cichu i zamienia właściwość w never (string, który jest też number). Błąd pojawia się dopiero później, przy próbie utworzenia wartości:

Błąd wskazuje na obiekt, daleko od deklaracji, która go spowodowała. Przy budowaniu typów obiektowych z innych typów obiektowych extends daje lepszy komunikat.

Sygnatury indeksu: łatwa do przeoczenia różnica

Alias typu obiektowego dostaje niejawną sygnaturę indeksu, więc można go przekazać tam, gdzie oczekiwany jest Record<string, unknown>. Interfejs jej nie dostaje. To celowe: zespół TypeScript wyjaśnia (issue #15300 w repozytorium TypeScript), że interfejs może zostać rozszerzony przez późniejsze deklaracje, więc wnioskowanie dla niego sygnatury indeksu jest mniej bezpieczne, a zmiana reguły teraz zepsułaby zbyt dużo kodu.

Wywołanie oznaczone @ts-expect-error i tak się wykonuje i wypisuje pola Alana: to reguła czasu kompilacji. To typowa przyczyna mylącego błędu, gdy wartość interfejsu trafia do funkcji pomocniczej do logowania, serializacji albo zapytań, otypowanej jako Record<string, ...>. Zmień tę jedną deklarację na type, rozwiń wartość spreadem albo otypuj parametr funkcji pomocniczej interfejsem lub generykiem.

Wydajność i komunikaty błędów

Strona Performance w wiki TypeScript (sekcja "Preferring Interfaces Over Intersections") zaleca interface Foo extends Bar, Baz { ... } zamiast type Foo = Bar & Baz & { ... } przy składaniu typów obiektowych. Powody: interfejs to jeden płaski typ obiektowy, który wykrywa konflikty właściwości, relacje typów między interfejsami są cache'owane (przecięcia jako całość nie), a sprawdzenie wartości względem przecięcia sprawdza każdy składnik przed spłaszczonym typem. Różnica ma znaczenie w dużych projektach z wieloma złożonymi typami; przy kilku zwykłych kształtach obiektów jej nie zmierzysz.

Ta sama strona zauważa, że interfejsy lepiej się wyświetlają. Interfejs jest pokazywany pod swoją nazwą w podpowiedziach i komunikatach błędów, a alias przecięcia często jest rozwijany na części, przez co długie komunikaty trudniej się czyta.

Czego używać

Reguła, która działa:

  1. Kształty obiektów: interface. Daje sprawdzane extends, czytelniejsze błędy przy dużych złożeniach i pozwala użytkownikom biblioteki go rozszerzać. To zgodne z heurystyką z handbooka TypeScript: „używaj interface, dopóki nie potrzebujesz funkcji type”.
  2. Wszystko inne: type. Unie, krotki, typy funkcji, typy literałowe i wszystko, co powstaje z mapped types, conditional types albo template literal types.
  3. Wyjątek: używaj type dla kształtu obiektu, który musi pasować do parametrów Record<string, ...>, albo gdy celowo chcesz zablokować łączenie.

Używanie type do wszystkiego to też spójny wybór i wiele projektów tak robi. Jedyna opcja, której warto unikać, to losowe mieszanie obu: czytelnik zaczyna się wtedy zastanawiać, czy różnica była zamierzona.

Najczęściej zadawane pytania

Jaka jest różnica między type a interface w TypeScript?

Oba opisują kształty obiektów i w tej roli są wymienne. type może też nazywać unie, krotki, prymitywy oraz mapped types i conditional types, czego interface nie potrafi. interface obsługuje łączenie deklaracji i extends, które sprawdza, czy właściwości nie są sprzeczne. Typy obiektowe zapisane przez type da się też przypisać do typów z sygnaturą indeksu, takich jak Record<string, unknown>, a interfejsów nie.

Używać type czy interface?

Heurystyka z handbooka TypeScript brzmi: używaj interface, dopóki nie potrzebujesz czegoś, co ma tylko type. W praktyce oznacza to interfejsy dla kształtów obiektów i type dla unii, krotek, typów funkcji i typów wyliczanych. Zespoły, które do wszystkiego używają type, też dobrze sobie radzą; ważne jest trzymanie się jednej spójnej reguły.

Czy interface jest szybszy niż type w TypeScript?

Przy składaniu typów obiektowych w niektórych przypadkach tak. Wytyczne zespołu TypeScript dotyczące wydajności zalecają interface ... extends zamiast dużych przecięć (A & B & { ... }), bo relacje między interfejsami są cache'owane, a interfejs jest jednym płaskim typem. Dla prostego kształtu obiektu nie ma odczuwalnej różnicy.

Czy klasa może implementować alias typu?

Tak, jeśli alias jest typem obiektowym (albo przecięciem typów obiektowych): class Point implements PointType { ... } działa. Klasa nie może implementować typu unii; to błąd TS2422.

Czy interfejs może rozszerzać alias typu?

Tak, o ile alias jest typem obiektowym: type Base = { id: string }; interface User extends Base { name: string } jest poprawne. Nie może rozszerzać aliasu unii. W drugą stronę alias typu może budować na interfejsie przez &.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ