Interfejs nadaje nazwę kształtowi obiektu: właściwościom, które musi mieć, i typowi każdej z nich. Po zadeklarowaniu używasz tej nazwy jako typu, a kompilator sprawdza względem niej każdy obiekt, który przekazujesz, zwracasz albo przypisujesz.
Ostatnie wywołanie to błąd kompilacji TS2741. Interfejsy są usuwane podczas kompilacji kodu: w wynikowym JavaScripcie nie ma śladu User i nic nie sprawdza kształtu w czasie działania.
Deklarowanie interfejsu
Składnia to słowo kluczowe interface, nazwa (zwyczajowo w PascalCase) i ciało z listą składowych. Składowe można oddzielać średnikami, przecinkami albo po prostu nowymi liniami; średniki to najczęstszy styl.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
Interfejs to typ, a nie wartość. Nie da się utworzyć jego instancji przez new, nie ma wartości domyślnych, a obj instanceof Product to błąd TS2693 ('Product' only refers to a type, but is being used as a value here). Żeby sprawdzić kształt w czasie działania, napisz type guard.
Typowanie strukturalne i sprawdzanie nadmiarowych właściwości
TypeScript porównuje kształty, a nie nazwy. Każdy obiekt z wymaganymi właściwościami pasuje do interfejsu, niezależnie od tego, czy został z nim zadeklarowany. Dodatkowe właściwości są dozwolone, z jednym wyjątkiem: literał obiektowy zapisany bezpośrednio tam, gdzie oczekiwany jest interfejs, przechodzi sprawdzenie nadmiarowych właściwości, bo nieznany klucz w takim miejscu to prawie zawsze literówka.
Ten błąd (TS2353) wyłapuje { id: 1, name: "a", emial: "x" } dla typu User: kompilator podpowiada nawet Did you mean to write 'email'? (TS2561).
Właściwości opcjonalne i readonly
? po nazwie czyni właściwość opcjonalną: obiekt może ją pominąć, a jej odczyt daje T | undefined. readonly zabrania ponownego przypisania właściwości po utworzeniu obiektu.
Widać tu dwa ograniczenia. Po pierwsze, readonly działa tylko w czasie kompilacji: dwie linie oznaczone @ts-expect-error i tak się wykonują po kliknięciu Run, i to z powodzeniem, a ostatnie linie zmieniają apiUrl przez referencję otypowaną bez readonly. Po drugie, jest płytkie: readonly hosts: string[] zablokowałoby ponowne przypisanie hosts, ale nadal pozwalałoby na hosts.push(...), dlatego sama tablica ma typ readonly string[]. readonly dokumentuje i egzekwuje zamiar w otypowanym kodzie; niczego nie zamraża. Utility type Readonly<T> sprawia, że wszystkie właściwości istniejącego interfejsu stają się naraz readonly.
Metody i właściwości funkcyjne
Metodę można zapisać jako sygnaturę metody, name(params): ReturnType, albo jako właściwość przechowującą funkcję, name: (params) => ReturnType. Wywołuje się je tak samo.
Różnica jest subtelna: przy strictFunctionTypes (część strict) parametry właściwości o typie funkcji są sprawdzane ściśle, a sygnatury metod luźniej (biwariantnie), więc forma właściwości wyłapuje kilka błędów więcej. Składnia metody jest krótsza i częściej spotykana; obie są w porządku.
Interfejs może też opisywać coś, co da się wywołać albo skonstruować, za pomocą sygnatury wywołania albo sygnatury konstrukcji:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Sygnatury indeksu
Gdy nazwy właściwości nie są znane z góry, sygnatura indeksu opisuje je wszystkie naraz: [key: string]: T oznacza „dowolny klucz typu string, każdy z wartością T”.
Ostatnie linie pokazują haczyk: odczyt nieistniejącego klucza ma typ number, a nie number | undefined. Opcja kompilatora noUncheckedIndexedAccess dodaje | undefined do każdego takiego odczytu.
Nazwane właściwości mogą stać obok sygnatury indeksu, ale muszą do niej pasować. interface Dict { [key: string]: number; name: string } to błąd TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Poszerz typ indeksu ([key: string]: number | string) albo przenieś dynamiczną część do osobnej właściwości. Dla prostych map klucz/wartość Record<string, number> mówi to samo w jednej linii.
Rozszerzanie interfejsu
extends buduje nowy interfejs z jednego lub kilku istniejących. Potomek ma wszystkie składowe rodzica i do tego własne:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
Potomek może ponownie zadeklarować właściwość rodzica tylko ze zgodnym (węższym) typem, na przykład kind: "dog" tam, gdzie rodzic ma kind: string. Reguły i sposób rozszerzania aliasów typów opisuje strona o extends.
Implementowanie interfejsu w klasie
class X implements Shape prosi kompilator o sprawdzenie, czy klasa ma wszystko, czego wymaga interfejs. Brakująca składowa to błąd przy deklaracji klasy:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
Po dodaniu area() kilka klas, a nawet zwykły obiekt, może służyć jako Shape:
implements to tylko sprawdzenie. Nie dodaje składowych do klasy i nie typuje za ciebie parametrów jej metod: greet(name) {} w klasie implementującej greet(name: string): string to nadal błąd TS7006, Parameter 'name' implicitly has an 'any' type. Klasa może implementować kilka interfejsów: class A implements B, C.
Łączenie deklaracji
Dwukrotne zadeklarowanie interfejsu o tej samej nazwie w tym samym zasięgu łączy obie deklaracje w jedną. Aliasy typów tego nie potrafią (drugi type o tej samej nazwie to błąd zduplikowanego identyfikatora).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
W kodzie aplikacji rzadko o to chodzi, a przypadkowe połączenie potrafi zmylić. Prawdziwe zastosowanie to dodawanie składowych do typów, które nie są twoje: opcji biblioteki albo globalnego obiektu, takiego jak Window. Wewnątrz modułu owiń deklarację w declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
Od tej chwili window.analytics.track("signup") przechodzi sprawdzanie typów w całym projekcie. Na tym samym mechanizmie opierają się pakiety z definicjami typów; zobacz pliki deklaracji.
Wartości domyślne właściwości interfejsu
Interfejs nie może przechowywać wartości domyślnych, bo opisuje typy i jest usuwany w czasie działania. size?: "sm" | "md" = "md" to błąd TS1246, An interface property cannot have an initializer. Uczyń właściwość opcjonalną i uzupełnij wartość domyślną tam, gdzie używasz obiektu:
Wartości domyślne przy destrukturyzacji są bezpieczniejsze: działają zawsze, gdy wartość to undefined, także przy jawnym size: undefined. Wersja ze spreadem kopiuje to jawne undefined na wartość domyślną, a jej wynik nadal ma typ, jakby size było zawsze ustawione. Jeśli potrzebujesz takiej gwarancji od typów, włącz exactOptionalPropertyTypes, które czyni size: undefined błędem kompilacji dla opcjonalnego size?: .... Klasa z zainicjalizowanymi polami to druga opcja, gdy obiekt potrzebuje też zachowania.
Interfejsy generyczne
Interfejs może przyjmować parametry typu, dzięki czemu jedna deklaracja działa dla wielu typów zawartości:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> czyta się jako „odpowiedź, której dane są stroną użytkowników”. Biblioteka standardowa jest ich pełna: Array<T>, Promise<T> i Map<K, V> to interfejsy generyczne.
Interfejs kontra alias typu
Alias type może opisywać ten sam kształt obiektu, a dla zwykłych typów obiektowych oba są wymienne. Tylko interfejs można łączyć; tylko alias typu może nazwać unię, krotkę albo mapped type czy conditional type. Praktyczna reguła z handbooka TypeScript: używaj interface, dopóki nie potrzebujesz czegoś, co ma tylko type. Pełne porównanie jest na stronie o interface vs type, łącznie z różnicą przy Record<string, ...>, która zaskakuje większość osób.
Najczęściej zadawane pytania
Czym jest interfejs w TypeScript?
Interfejs to nazwany opis kształtu obiektu: nazw jego właściwości, ich typów, tego, które są opcjonalne albo readonly, oraz jego metod. Kompilator sprawdza, czy wartości używane jako ten interfejs mają taki kształt. Interfejsy istnieją tylko w czasie kompilacji; nie generują żadnego JavaScriptu.
Jak ustawić wartość domyślną w interfejsie TypeScript?
Nie da się: interfejs opisuje typy, a nie wartości, więc size: "md" = ... to niepoprawna składnia. Oznacz właściwość jako opcjonalną (size?: "sm" | "md") i zastosuj wartość domyślną tam, gdzie używasz obiektu, zwykle przez wartości domyślne przy destrukturyzacji parametrów funkcji: function render({ size = "md" }: Options). Rozwinięcie obiektu z wartościami domyślnymi ({ ...DEFAULTS, ...options }) też działa, ale jawne undefined w options nadpisuje wartość domyślną.
Jak sprawdzić w czasie działania, czy obiekt implementuje interfejs?
Nie ma wbudowanego sposobu, bo interfejsy są usuwane podczas kompilacji: obj instanceof User to błąd TS2693 ('User' only refers to a type, but is being used as a value here). Napisz funkcję type guard, która sprawdza właściwości, function isUser(x: unknown): x is User { ... }, albo waliduj przez bibliotekę schematów.
Czy interfejs może rozszerzać kilka interfejsów?
Tak. Wypisz je po extends, oddzielone przecinkami: interface ServiceDog extends Pet, Trained { ... }. Nowy interfejs ma wszystkie składowe każdego rodzica i do tego własne. Jeśli dwaj rodzice deklarują tę samą właściwość z niezgodnymi typami, deklaracja jest błędem.
Czym różni się interfejs od klasy w TypeScript?
Klasa istnieje w czasie działania: ma konstruktor, implementacje metod, a new tworzy z niej obiekty. Interfejs tylko opisuje kształt dla kompilatora i jest usuwany z wynikowego JavaScriptu. Klasa może zadeklarować implements SomeInterface, żeby kompilator sprawdził, czy pasuje, a każdy zwykły obiekt o właściwym kształcie też pasuje do interfejsu.