Interfejs TypeScript a typ: Rzeczywiste różnice przetestowane za pomocą kompilatora
Interfejsy i aliasy typów nakładają się na siebie w tak dużym stopniu, że wybór często wydaje się arbitralny. Oto, co każdy może zrobić, czego nie może zrobić drugi, co potwierdzają rzeczywiste błędy kompilatora, a także prosta zasada, której może przestrzegać Twój zespół.
- W przypadku prostych kształtów obiektów interfejs i alias typu są prawie zamienne.
- Tylko
typemoże nazywać unii, krotki i inne typy nieobiektowe. - Tylko
interfaceobsługuje łączenie deklaracji, w przypadku którego łączą się dwie deklaracje. - Powszechna zasada: interfejsy dla kształtów obiektów,
typedla wszystkiego innego, stosowane konsekwentnie.
Niewiele pytań dotyczących TypeScriptu wywołuje tyle dyskusji, co Interfejs TypeScriptu a typ. Wiele artykułów wymienia różnice, które kiedyś miały znaczenie w starszych wersjach, ale są teraz nieaktualne. Zamiast powtarzać twierdzenia, przetestowaliśmy je.
Napisaliśmy jeden plik ćwiczący każdą różnicę i sprawdziliśmy go w TypeScript 5.6.3 w trybie ścisłym. Błędy kompilatora cytowane poniżej pochodzą z tego uruchomienia. Zachowanie może się zmieniać między wersjami, więc sprawdź wersję TypeScript, z której korzysta Twój projekt. Jeśli dopiero zaczynasz przygodę z tym językiem, zacznij od naszego samouczka TypeScript dla początkujących.
Krótka odpowiedź
interface opisuje kształt obiektu, który można przedłużyć lub ponownie otworzyć. Alias type nadaje nazwę dowolnemu typowi, w tym obiektom, uniom, krotkom i typom funkcji. Tam, gdzie oba mogą działać, różnica polega głównie na stylu.
Gdzie są takie same
Te dwie deklaracje opisują identyczne kształty:
interface User {
id: number;
name: string;
email?: string;
}
type UserAlias = {
id: number;
name: string;
email?: string;
};
const a: User = { id: 1, name: "Asha" };
const b: UserAlias = a; // no error
TypeScript używa typowania strukturalnego, co oznacza, że porównuje kształty, a nie nazwy. Dlatego User można bez problemu przypisać do UserAlias. Obydwa obsługują opcjonalne właściwości, readonly właściwości, metody i sygnatury indeksów.
Interfejs vs typ obok siebie
| Funkcja | interfejs | wpisz |
|---|---|---|
| Kształty obiektów | Tak | Tak |
Związki, takie jak "a" | "b" |
Nie | Tak |
| Krotki i prymitywy | Nie | Tak |
| Typy mapowane i warunkowe | Nie | Tak |
| Łączenie deklaracji | Tak | Nie, to błąd |
| Rozszerzanie | rozciąga się |
& skrzyżowanie |
| Sprzeczne właściwości | Błąd w deklaracji | Może po cichu stać się never |
Używany z implements |
Tak | Tak, jeśli jest to typ obiektowy |
Co może zrobić tylko typ
Wiele przydatnych typów w ogóle nie jest kształtami obiektów i tylko alias typu może je nazwać:
type Status = "pending" | "paid" | "shipped"; // union
type Point = [number, number]; // tuple
type Handler = (event: string) => void; // function type
type Keys = keyof User; // computed type
type ReadonlyUser = Readonly<User>; // utility type
Typy unii należą do najcenniejszych funkcji TypeScriptu, ponieważ kompilator odrzuca wówczas każdą wartość spoza listy:
let s: Status = "paid";
s = "refunded";
// error TS2322: Type '"refunded"' is not assignable to type 'Status'.
Wychwytuje literówki i nieaktualne wartości w czasie kompilacji, zanim trafią one do produkcji.
Łączenie deklaracji
Jeśli dwukrotnie zadeklarujesz interfejs o tej samej nazwie, TypeScript połączy je w jeden:
interface Settings {
theme: string;
}
interface Settings {
language: string;
}
const settings: Settings = { theme: "dark", language: "en" }; // no error
Spróbuj tego samego z aliasem typu, a kompilator odmawia:
type Config = { theme: string };
type Config = { language: string };
// error TS2300: Duplicate identifier 'Config'.
Scalanie to sposób, w jaki biblioteki pozwalają rozszerzać swoje typy. Na przykład możesz dodać niestandardową właściwość do interfejsu globalnego Window lub do obiektu żądania struktury bez modyfikowania biblioteki. To naprawdę przydatne.
Drugą stroną jest to, że połączenie może nastąpić przez przypadek. Jeśli dwa pliki w projekcie zadeklarują interfejs o tej samej nazwie w tym samym zakresie, zostaną one po cichu połączone, co może być mylące. Alias typu powoduje, że kolizja staje się zamiast tego błędem.
rozciąga się a przecina
Oba pozwalają budować na istniejącym typie:
interface Admin extends User {
permissions: string[];
}
type AdminAlias = UserAlias & { permissions: string[] };
W przypadku zgodnych właściwości wynik jest taki sam. Różnica pojawia się w przypadku konfliktu. Gdy interfejs próbuje w sposób niezgodny zmienić typ właściwości, kompilator natychmiast to zgłasza:
interface Base { id: number }
interface Broken extends Base { id: string }
// error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
// Types of property 'id' are incompatible.
// Type 'string' is not assignable to type 'number'.
Skrzyżowanie nie narzeka, gdy jest zadeklarowane. Zamiast tego właściwość powodująca konflikt staje się never, typem, którego nie może mieć żadna wartość. Błąd pojawia się dopiero później, gdy spróbujesz go użyć:
type Mixed = { id: number } & { id: string };
const m: Mixed = { id: 1 };
// error TS2322: Type 'number' is not assignable to type 'never'.
Ten późniejszy błąd jest bardziej zagmatwany, ponieważ odwraca uwagę od rzeczywistego błędu. Jest to jeden z praktycznych powodów, dla których warto preferować extends podczas tworzenia typów obiektów.
Zajęcia i narzędzia
Klasa może obiecać dopasowanie kształtu za pomocą implements i działa to z interfejsem lub aliasem typu obiektu:
class Account implements User {
constructor(public id: number, public name: string) {}
}
To skompilowało się bez błędów. Zauważ, że implements sprawdza tylko, czy klasa jest zgodna. Nie dodaje żadnych właściwości ani kodu. Klasa nie może zaimplementować typu unii, ponieważ nie ma jednego pasującego kształtu.
Tylko do odczytu i typy narzędzi
Typy narzędzi, takie jak Readonly, Partial, Pick i Omit działają zarówno z interfejsami, jak i aliasami. Wynikiem jest nowy typ, którego nazwa zwykle zawiera alias typu:
const r: ReadonlyUser = { id: 3, name: "Dev" };
r.name = "Chitra";
// error TS2540: Cannot assign to 'name' because it is a read-only property.
Pamiętaj, że readonly to tylko kontrola w czasie kompilacji. W czasie wykonywania obiektem jest zwykły JavaScript i nadal można go zmienić za pomocą kodu omijającego typy.
Wskazówka: Typy znikają, gdy TypeScript kompiluje się do JavaScript. Dane, które przychodzą z zewnątrz, takie jak odpowiedź API, nie są sprawdzane w czasie wykonywania tylko dlatego, że mają typ. Sprawdź to, na przykład za pomocą biblioteki schematów. W naszym samouczku dotyczącym pobierania interfejsu API opisano bezpieczną obsługę odpowiedzi interfejsu API.
Wydajność kompilatora
Własne wskazówki dotyczące wydajności zespołu TypeScript sugerują preferowanie interfejsów korzystających z extends zamiast dużych skrzyżowań, ponieważ kompilator może wydajniej buforować relacje interfejsów. W małych i średnich projektach raczej nie zauważysz żadnej różnicy. Może to mieć znaczenie w przypadku bardzo dużych baz kodu z głęboko skomponowanymi typami. Sami tego nie mierzyliśmy.
Którego powinieneś użyć?
Powszechnie stosowaną regułą, zgodną ze wskazówkami zawartymi w podręczniku TypeScript, jest:
- Użyj
interfacew przypadku kształtów obiektów, zwłaszcza publicznych interfejsów API i wszystkiego, co implementują klasy lub inne mogą rozszerzać. - Użyj
typew przypadku unii, krotek, typów funkcji oraz typów mapowanych i obliczanych.
| Ty definiujesz | Użyj |
|---|---|
| Model danych, taki jak Użytkownik lub Zamówienie | interfejs |
| Zbiór dozwolonych wartości | type związek |
| Para o stałej długości lub potrójna | type krotka |
| Podpis wywołania zwrotnego | wpisz |
| Rozszerzanie typów bibliotek | interface, poprzez połączenie |
| Odmiana istniejącego typu | type z typem użytkowym |
Niektóre zespoły po prostu używają type wszędzie i to również działa dobrze. Najważniejsza jest spójność, więc wybierz konwencję, zapisz ją w swoim przewodniku po stylu i pozwól, aby linter ją egzekwował. Może to zrobić reguła ESLint consistent-type-definitions z TypeScript-eslint.
Typowe błędy
- Niekończąca się debata. Obydwa są dobre dla obiektów, więc ustal raz regułę i idź dalej.
- Przypadkowe połączenie. Ponowne użycie nazwy interfejsu w tym samym zakresie powoduje ich ciche połączenie.
- Sprzeczne skrzyżowania. Tworzą
nevertypy z mylącymi błędami później. - Ufanie typom w czasie wykonywania. Są one usuwane, więc sprawdź dane zewnętrzne.
- Użycie
anydo wyciszenia błędów. Wyłącza sprawdzanie. Wybierzunknowni zawęź go.
Przełączanie między nimi w istniejącej bazie kodu
Zmiana interface na type lub odwrotnie jest zwykle bezpieczna w przypadku zwykłych kształtów obiektów, ponieważ TypeScript porównuje typy według ich struktury, a nie nazw. Przed zmianą należy jeszcze sprawdzić kilka rzeczy:
- Konwersja interfejsu na alias typu usuwa łączenie deklaracji, więc każda druga deklaracja o tej samej nazwie staje się błędem zduplikowanego identyfikatora.
- Konwersja aliasu typu na interfejs działa tylko w przypadku kształtów obiektów. Unie, krotki i typy mapowane muszą pozostać aliasami typów.
- Komunikaty o błędach i kursory edytora mogą wyświetlać typ w inny sposób, co może zaskoczyć członków drużyny, ale nie zmienia zachowania.
Wprowadź zmianę w jednym skupionym żądaniu ściągnięcia, uruchom kompilator w trybie ścisłym i w zestawie testów i unikaj mieszania tego z pracą nad funkcjami, aby recenzenci mogli zobaczyć, że nic więcej się nie zmieniło.
Podsumowanie
Pytanie dotyczące interfejsu a typu ma spokojną odpowiedź: pokrywają się one w przypadku obiektów, każdy ma kilka unikalnych zdolności, a spójność jest ważniejsza niż wybór. Użyj interfejsów dla kształtów i typów do wszystkiego innego, chyba że Twój zespół woli inaczej. Aby samemu zobaczyć różnice, skopiuj powyższe przykłady do pliku i uruchom na nim tsc --noEmit --strict.
Najczęstsze pytania
Jaka jest różnica między interfejsem a typem w TypeScript?
Obydwa potrafią opisywać kształty obiektów. Tylko typ może nazywać unii, krotki i typy obliczone, podczas gdy tylko interfejs obsługuje łączenie deklaracji.
Czy powinienem używać interfejsu lub typu dla rekwizytów React?
Albo działa dobrze. Wiele zespołów używa interfejsów jako rekwizytów, podczas gdy inne wolą typ ze względu na spójność ze związkami. Wybierz jeden i zastosuj go w całym projekcie.
Czy typ może rozszerzać interfejs?
Tak, używając skrzyżowania, takiego jak Admin = User & { uprawnienia: string[] }. Interfejs może także rozszerzać alias typu obiektowego.
Czy interfejs jest szybszy niż typ?
Wskazówki dotyczące wydajności TypeScript faworyzują interfejs rozciągający się na duże przecięcia w dużych bazach kodu. W typowych projektach różnica jest mało zauważalna.
Co to jest łączenie deklaracji?
Dzieje się tak, gdy dwa interfejsy o tej samej nazwie i tym samym zakresie łączą się w jeden. Aliasy typów nie mogą się łączyć, a zduplikowane nazwy powodują błąd.
Ucz się dalej
Potrzebujesz pomocy przy testowaniu, WordPressie lub SEO?
Pomagamy zespołom dostarczać lepsze oprogramowanie i rozwijać ich witryny. Powiedz nam, nad czym pracujesz, a my zaproponujemy jasny następny krok.
Więcej poradników na testowanie, sztuczna inteligencja i nowoczesny rozwój
Podobał Ci się ten artykuł? Wybierz temat i przejdź bezpośrednio do naszych najnowszych tutoriali.
Wyjaśnienie halucynacji AI: dlaczego modele językowe tworzą rzeczy i jak je zmniejszyć
Wyjaśniono halucynacje AI: czym są, dlaczego powstają w modelach językowych, typowe typy, jak je zredukować za pomocą uziemień i podpowiedzi oraz pr…
Przeczytaj artykuł →DevOpsWyjaśnienie zmiennych środowiskowych: pliki .env, sekrety i konfiguracja wykonana prawidłowo
Zmienne środowiskowe wyjaśnione na przykładach Pythona, które przeprowadziliśmy: ich odczytywanie i ustawianie, dlaczego każda wartość jest ciągiem …
Przeczytaj artykuł →Tworzenie oprogramowaniaWyjaśnienie ograniczenia szybkości interfejsu API: algorytmy, nagłówki i obsługa błędów 429
Ograniczanie szybkości API wyjaśnione za pomocą przeprowadzonej przez nas symulacji: zasobnik tokenów, stałe okno, przesuwane okno, 429 odpowiedzi, …
Przeczytaj artykuł →Automatyzacja testówWyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią
Wyśmiewanie w Pythonie za pomocą unittest.mock, przetestowane: Mock i zwracana wartość, efekt uboczny, łatanie tam, gdzie używana jest nazwa, specyf…
Przeczytaj artykuł →Testowanie oprogramowaniaWyjaśnienie zasięgu kodu: pokrycie linii i oddziału oraz co naprawdę oznacza 100%
Wyjaśnienie pokrycia kodu za pomocą prawdziwego pliku cover.py: pokrycie linii, gałęzi i warunków, przeczytanie raportu, dlaczego 100% pokrycia może…
Przeczytaj artykuł →Bazy danych i SQLDELETE vs TRUNCATE vs DROP w SQL: różnice, wycofywanie i kiedy używać każdego z nich
DELETE vs TRUNCATE vs DROP wyjaśnione za pomocą przetestowanego SQL: co każdy usuwa, wycofywanie, wyzwalacze, resetowanie tożsamości, prędkość i czy…
Przeczytaj artykuł →Bazy danych i SQLWyjaśnienie widoków SQL: tworzenie, używanie i unikanie pułapek wraz z przykładami
Widoki SQL wyjaśnione za pomocą zapytań, które uruchomiliśmy: tworzenie widoku, wyniki na żywo, ukrywanie kolumn, wydajność, widoki z możliwością ak…
Przeczytaj artykuł →Tworzenie oprogramowaniaInterfejs TypeScript a typ: rzeczywiste różnice, testowane za pomocą kompilatora
Interfejs TypeScript a typ, sprawdzony z prawdziwym kompilatorem: co oba mogą zrobić, co tylko jeden, łączenie deklaracji, rozszerzenia a przecięcia…
Przeczytaj artykuł →Tworzenie oprogramowaniaPython *args i **kwargs wyjaśnione: elastyczne funkcje z przejrzystymi przykładami
Python *args i **kwargs wyjaśnione za pomocą uruchomionego przez nas kodu: pakowanie i rozpakowywanie, kolejność parametrów, argumenty zawierające t…
Przeczytaj artykuł →Tworzenie oprogramowaniaFunkcje Lambda w Pythonie: składnia, rzeczywiste zastosowania i kiedy ich nie używać
Funkcje lambda w Pythonie wyjaśnione za pomocą uruchomionego przez nas kodu: składnia, sortowanie za pomocą klucza, mapa i filtr, domknięcia, pułapk…
Przeczytaj artykuł →Sztuczna inteligencjaKim są agenci AI? Jak one działają, dzięki prostej pętli agenta, którą możesz uruchomić
Kim są agenci AI? Prosty przewodnik po pętli agenta, narzędziach, pamięci i poręczach, z małą pętlą w Pythonie, którą uruchomiliśmy, prawdziwymi zag…
Przeczytaj artykuł →DevOpsMonitorowanie a obserwowalność: prawdziwa różnica z logami, wskaźnikami i śladami
Wyjaśnienie monitorowania a obserwowalności: znane i nieznane awarie, dzienniki, metryki i ślady, opis incydentu przy kasie, złote sygnały, OpenTele…
Przeczytaj artykuł →DevOpsWoluminy platformy Docker a mocowania powiązań: utrwalanie danych kontenera we właściwy sposób
Wyjaśniono woluminy Dockera a montowanie powiązań: dlaczego dane kontenera znikają, nazwane woluminy, montowanie powiązań, tmpfs, przykłady tworzeni…
Przeczytaj artykuł →Automatyzacja testówTDD vs BDD: kluczowe różnice, jedna funkcja zbudowana w obie strony i kiedy używać każdej z nich
TDD vs BDD wyjaśnione za pomocą jednej funkcji rabatowej zbudowanej w obie strony: refaktoryzacja czerwono-zielona z uruchomionym przez nas kodem, s…
Przeczytaj artykuł →Tworzenie oprogramowaniaGit merge vs rebase: różnice, przykłady i kiedy czego używać
Git merge vs rebase wyjaśnione na prawdziwym wyniku logu, z tabelą porównawczą, złotą zasadą rebase i bezpiecznym procesem pracy zespołu.
Przeczytaj artykuł →Tworzenie oprogramowaniaPython list comprehension: składnia, przykłady i kiedy jej unikać
Python list comprehension wyjaśnione: składnia, filtrowanie, if-else, zagnieżdżone pętle, zmierzone czasy i sytuacje, w których zwykła pętla for jes…
Przeczytaj artykuł →Bazy danych i SQLSQL JOIN – wyjaśnienie z przykładami (Inner, Left, Right, Full)
Zrozum złączenia SQL krok po kroku na przykładowych tabelach i zapytaniach: INNER, LEFT, RIGHT, FULL, CROSS i SELF, plus typowe błędy i wskazówki wy…
Przeczytaj artykuł →