Poznawanie systemu typów
Usługa językowa TypeScript
Usługa językowa TypeScript, znana również jako tsserver, oferuje różne funkcje, takie jak raportowanie błędów, diagnostyka, kompilacja przy zapisie, zmiana nazw, przechodzenie do definicji, listy uzupełnień, podpowiedzi dotyczące sygnatur i inne. Jest używana przede wszystkim przez zintegrowane środowiska programistyczne (IDE) do obsługi IntelliSense. Płynnie integruje się z Visual Studio Code i jest wykorzystywana przez narzędzia takie jak Conquer of Completion (Coc).
Programiści mogą korzystać ze specjalnego API i tworzyć własne wtyczki usługi językowej, aby usprawnić pracę z TypeScript podczas edycji kodu. Może to być szczególnie przydatne do implementowania specjalnych funkcji lintowania lub włączania automatycznego uzupełniania dla niestandardowego języka szablonów.
Przykładem niestandardowej wtyczki używanej w rzeczywistych projektach jest „typescript-styled-plugin”, która zapewnia raportowanie błędów składniowych oraz obsługę IntelliSense dla właściwości CSS w komponentach stylizowanych.
Więcej informacji i przewodniki szybkiego startu można znaleźć w oficjalnej witrynie Wiki TypeScript w serwisie GitHub: https://github.com/microsoft/TypeScript/wiki/.
Typowanie strukturalne
TypeScript opiera się na strukturalnym systemie typów. Oznacza to, że zgodność i równoważność typów są określane na podstawie rzeczywistej struktury lub definicji typu, a nie jego nazwy czy miejsca deklaracji, jak w nominalnych systemach typów, takich jak C# lub C.
Strukturalny system typów TypeScript został zaprojektowany na podstawie sposobu, w jaki działa dynamiczne kacze typowanie w JavaScript w czasie wykonywania.
Poniższy przykład jest poprawnym kodem TypeScript. Jak widać, „X” i „Y” mają ten sam element składowy „a”, mimo że ich deklaracje mają różne nazwy. Typy są określane na podstawie ich struktur, a ponieważ w tym przypadku struktury są takie same, typy są zgodne i poprawne.
type X = { a: string;};type Y = { a: string;};const x: X = { a: 'a' };const y: Y = x; // PoprawnePodstawowe reguły porównywania w TypeScript
Proces porównywania w TypeScript jest rekurencyjny i wykonywany dla typów zagnieżdżonych na dowolnym poziomie.
Typ „X” jest zgodny z typem „Y”, jeśli „Y” ma co najmniej te same elementy składowe co „X”.
type X = { a: string;};const y = { a: 'A', b: 'B' }; // Poprawne, ponieważ zawiera co najmniej te same składowe co Xconst r: X = y;Parametry funkcji są porównywane według typów, a nie według ich nazw:
type X = (a: number) => void;type Y = (a: number) => void;let x: X = (j: number) => undefined;let y: Y = (k: number) => undefined;y = x; // Poprawnex = y; // PoprawneTypy zwracane przez funkcje muszą być takie same:
type X = (a: number) => undefined;type Y = (a: number) => number;let x: X = (a: number) => undefined;let y: Y = (a: number) => 1;y = x; // Niepoprawnex = y; // NiepoprawneTyp zwracany przez funkcję źródłową musi być podtypem typu zwracanego przez funkcję docelową:
let x = () => ({ a: 'A' });let y = () => ({ a: 'A', b: 'B' });x = y; // Poprawney = x; // Niepoprawne, brakuje składowej bPomijanie parametrów funkcji jest dozwolone, ponieważ jest to powszechna praktyka w JavaScript, na przykład podczas używania „Array.prototype.map()”:
[1, 2, 3].map((element, _index, _array) => element + 'x');Dlatego poniższe deklaracje typów są całkowicie poprawne:
type X = (a: number) => undefined;type Y = (a: number, b: number) => undefined;let x: X = (a: number) => undefined;let y: Y = (a: number) => undefined; // Brak parametru by = x; // PoprawneWszelkie dodatkowe parametry opcjonalne typu źródłowego są poprawne:
type X = (a: number, b?: number, c?: number) => undefined;type Y = (a: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Poprawnex = y; //PoprawneWszelkie parametry opcjonalne typu docelowego, którym nie odpowiadają parametry w typie źródłowym, są poprawne i nie powodują błędu:
type X = (a: number) => undefined;type Y = (a: number, b?: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Poprawnex = y; // PoprawneParametr resztowy jest traktowany jako nieskończona seria parametrów opcjonalnych:
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //poprawneFunkcje z przeciążeniami są poprawne, jeśli sygnatura przeciążenia jest zgodna z sygnaturą implementacji:
function x(a: string): void;function x(a: string, b: number): void;function x(a: string, b?: number): void { console.log(a, b);}x('a'); // Poprawnex('a', 1); // Poprawne
function y(a: string): void; // Niepoprawne, niezgodne z sygnaturą implementacjifunction y(a: string, b: number): void;function y(a: string, b: number): void { console.log(a, b);}y('a');y('a', 1);Porównanie parametrów funkcji kończy się powodzeniem, jeśli parametry źródłowe i docelowe można przypisać do nadtypów lub podtypów (biwariancja).
// Nadtypclass X { a: string; constructor(value: string) { this.a = value; }}// Podtypclass Y extends X {}// Podtypclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// Biwariancja akceptuje nadtypyconsole.log(getA(new X('x'))); // Poprawneconsole.log(getA(new Y('Y'))); // Poprawneconsole.log(getA(new Z('z'))); // PoprawneTypy wyliczeniowe są porównywalne i zgodne z liczbami, a liczby z typami wyliczeniowymi, ale porównywanie wartości pochodzących z różnych typów wyliczeniowych jest niepoprawne.
enum X { A, B,}enum Y { A, B, C,}const xa: number = X.A; // Poprawneconst ya: Y = 0; // PoprawneX.A === Y.A; // NiepoprawneInstancje klasy podlegają kontroli zgodności swoich prywatnych i chronionych elementów składowych:
class X { public a: string; constructor(value: string) { this.a = value; }}
class Y { private a: string; constructor(value: string) { this.a = value; }}
let x: X = new Y('y'); // NiepoprawneKontrola porównawcza nie uwzględnia różnic w hierarchii dziedziczenia, na przykład:
class X { public a: string; constructor(value: string) { this.a = value; }}class Y extends X { public a: string; constructor(value: string) { super(value); this.a = value; }}class Z { public a: string; constructor(value: string) { this.a = value; }}let x: X = new X('x');let y: Y = new Y('y');let z: Z = new Z('z');x === y; // Poprawnex === z; // Poprawne, nawet jeśli z należy do innej hierarchii dziedziczeniaTypy generyczne są porównywane na podstawie swoich struktur, z uwzględnieniem typu wynikowego otrzymanego po zastosowaniu parametru typu. Porównywany jest wyłącznie końcowy wynik jako typ niegeneryczny.
interface X<T> { a: T;}let x: X<number> = { a: 1 };let y: X<string> = { a: 'a' };x === y; // Niepoprawne, ponieważ argument typu jest używany w strukturze końcowejinterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Poprawne, ponieważ argument typu nie jest używany w strukturze końcowejJeśli argument typu generycznego nie został określony, wszystkie nieokreślone argumenty są traktowane jako typy any:
type X = <T>(x: T) => T;type Y = <K>(y: K) => K;let x: X = x => x;let y: Y = y => y;x = y; // PoprawneZapamiętaj:
let a: number = 1;let b: number = 2;a = b; // Poprawne, wszystko można przypisać do siebie samego
let c: any;c = 1; // Poprawne, wszystkie typy można przypisać do any
let d: unknown;d = 1; // Poprawne, wszystkie typy można przypisać do unknown
let e: unknown;let e1: unknown = e; // Poprawne, unknown można przypisać tylko do siebie i do anylet e2: any = e; // Poprawnelet e3: number = e; // Niepoprawne
let f: never;f = 1; // Niepoprawne, niczego nie można przypisać do never
let g: void;let g1: any;g = 1; // Niepoprawne, void nie można przypisać do innych typów ani odwrotnie, z wyjątkiem anyg = g1; // PoprawneNależy pamiętać, że gdy opcja „strictNullChecks” jest włączona, wartości „null” i „undefined” są traktowane podobnie do „void”; w przeciwnym razie są traktowane podobnie do „never”.
Typy jako zbiory
W TypeScript typ jest zbiorem możliwych wartości. Zbiór ten jest również nazywany dziedziną typu. Każdą wartość danego typu można postrzegać jako element zbioru. Typ określa ograniczenia, które musi spełniać każdy element zbioru, aby można go było uznać za należący do tego zbioru. Podstawowym zadaniem TypeScript jest sprawdzanie i weryfikowanie, czy jeden zbiór jest podzbiorem innego.
TypeScript obsługuje różne rodzaje zbiorów:
| Pojęcie z teorii zbiorów | TypeScript | Uwagi |
|---|---|---|
| Zbiór pusty | never | „never” nie zawiera niczego poza samym sobą |
| Zbiór jednoelementowy | undefined / null / typ literałowy | |
| Zbiór skończony | boolean / unia | |
| Zbiór nieskończony | string / number / object | |
| Zbiór uniwersalny | any / unknown | Każdy element należy do „any” i każdy zbiór jest jego podzbiorem / „unknown” jest bezpiecznym typowo odpowiednikiem „any” |
Oto kilka przykładów:
| TypeScript | Pojęcie z teorii zbiorów | Przykład |
|---|---|---|
| never | ∅ (zbiór pusty) | const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’ |
| Typ literałowy | Zbiór jednoelementowy | type X = ‘X’; |
| type Y = 7; | ||
| Wartość przypisywalna do T | Wartość ∈ T (należy do) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| T1 przypisywalny do T2 | T1 ⊆ T2 (podzbiór) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| const j: XY = ‘J’; // Type ‘“J”’ is not assignable to type ‘XY’. | ||
| T1 extends T2 | T1 ⊆ T2 (podzbiór) | type X = ‘X’ extends string ? true : false; |
| T1 | T2 | T1 ∪ T2 (unia) | type XY = ‘X’ | ‘Y’; |
| type JK = 1 | 2; | ||
| T1 & T2 | T1 ∩ T2 (przecięcie) | type X = { a: string } |
| type Y = { b: string } | ||
| type XY = X & Y | ||
| const x: XY = { a: ‘a’, b: ‘b’ } | ||
| unknown | Zbiór uniwersalny | const x: unknown = 1 |
Unia (T1 | T2) tworzy szerszy zbiór (obejmujący oba typy):
type X = { a: string;};type Y = { b: string;};type XY = X | Y;const r: XY = { a: 'a', b: 'x' }; // PoprawnePrzecięcie (T1 & T2) tworzy węższy zbiór (tylko część wspólną):
type X = { a: string;};type Y = { a: string; b: string;};type XY = X & Y;const r: XY = { a: 'a' }; // Niepoprawneconst j: XY = { a: 'a', b: 'b' }; // PoprawneSłowo kluczowe extends można w tym kontekście rozumieć jako „jest podzbiorem”. Nakłada ono ograniczenie na typ. Gdy extends jest używane z typem generycznym, ogranicza parametr typu generycznego do bardziej szczegółowego typu.
Należy pamiętać, że extends nie ma tutaj nic wspólnego z dziedziczeniem klas w rozumieniu programowania obiektowego.
TypeScript korzysta z typów strukturalnych i nie ma ścisłej hierarchii nominalnej. Jak pokazuje poniższy przykład, dwa typy mogą się nakładać, mimo że żaden z nich nie jest podtypem drugiego, ponieważ TypeScript bierze pod uwagę strukturę, czyli kształt obiektów.
interface X { a: string;}interface Y extends X { b: string;}interface Z extends Y { c: string;}const z: Z = { a: 'a', b: 'b', c: 'c' };interface X1 { a: string;}interface Y1 { a: string; b: string;}interface Z1 { a: string; b: string; c: string;}const z1: Z1 = { a: 'a', b: 'b', c: 'c' };
const r: Z1 = z; // PoprawnePrzypisywanie typu: deklaracje typów i asercje typów
W TypeScript typ można przypisać na różne sposoby:
Deklaracja typu
W poniższym przykładzie używamy x: X („: Type”), aby zadeklarować typ zmiennej x.
type X = { a: string;};
// Deklaracja typuconst x: X = { a: 'a',};Jeśli zmienna nie ma określonego formatu, TypeScript zgłosi błąd. Na przykład:
type X = { a: string;};
const x: X = { a: 'a', b: 'b', // Błąd: Literał obiektu może zawierać tylko znane właściwości};Asercja typu
Asercję można dodać za pomocą słowa kluczowego as. Informuje to kompilator, że programista ma więcej informacji o typie i wycisza wszelkie błędy, które mogą wystąpić.
Na przykład:
type X = { a: string;};const x = { a: 'a', b: 'b',} as X;W powyższym przykładzie za pomocą słowa kluczowego as określono, że obiekt x ma typ X. Informuje to kompilator TypeScript, że obiekt jest zgodny z określonym typem, mimo że ma dodatkową właściwość b, której nie ma w definicji typu.
Asercje typów są przydatne w sytuacjach, w których trzeba określić bardziej szczegółowy typ, zwłaszcza podczas pracy z DOM. Na przykład:
const myInput = document.getElementById('my_input') as HTMLInputElement;W tym przypadku asercja typu as HTMLInputElement informuje TypeScript, że wynik getElementById należy traktować jako HTMLInputElement. Asercji typów można również używać do zmiany mapowania kluczy, jak pokazano w poniższym przykładzie z literałami szablonowymi:
type J<Type> = { [ Property in keyof Type as `prefix_${string & Property}` ]: () => Type[Property];};type X = { a: string; b: number;};type Y = J<X>;W tym przykładzie typ J<Type> używa typu mapowanego z literałem szablonowym, aby ponownie zmapować klucze typu Type. Tworzy nowe właściwości z prefiksem „prefix_” dodanym do każdego klucza, a odpowiadające im wartości są funkcjami zwracającymi oryginalne wartości właściwości.
Warto zauważyć, że podczas używania asercji typu TypeScript nie przeprowadza kontroli nadmiarowych właściwości. Dlatego gdy struktura obiektu jest znana z wyprzedzeniem, zasadniczo lepiej jest użyć deklaracji typu.
Deklaracje otoczenia
Deklaracje otoczenia to pliki opisujące typy kodu JavaScript; ich nazwy mają format .d.ts. Zazwyczaj są importowane i używane do opisywania typami istniejących bibliotek JavaScript lub do dodawania typów do istniejących plików JS w projekcie.
Typy dla wielu popularnych bibliotek można znaleźć pod adresem: https://github.com/DefinitelyTyped/DefinitelyTyped/
i zainstalować za pomocą polecenia:
npm install --save-dev @types/library-nameWłasne deklaracje otoczenia można importować za pomocą odwołania z potrójnym ukośnikiem:
/// <reference path="./library-types.d.ts" />Deklaracji otoczenia można używać nawet w plikach JavaScript za pomocą // @ts-check.
Słowo kluczowe declare umożliwia definiowanie typów dla istniejącego kodu JavaScript bez jego importowania, pełniąc funkcję symbolu zastępczego dla typów pochodzących z innego pliku lub dostępnych globalnie.
Kontrola właściwości i kontrola nadmiarowych właściwości
TypeScript opiera się na strukturalnym systemie typów, ale kontrola nadmiarowych właściwości jest funkcją TypeScript, która pozwala sprawdzić, czy obiekt ma dokładnie właściwości określone w typie.
Kontrola nadmiarowych właściwości jest przeprowadzana na przykład podczas przypisywania literałów obiektowych do zmiennych lub przekazywania ich jako argumentów do nadmiarowej właściwości funkcji.
type X = { a: string;};const y = { a: 'a', b: 'b' };const x: X = y; // Poprawne dzięki typowaniu strukturalnemuconst w: X = { a: 'a', b: 'b' }; // Niepoprawne z powodu sprawdzania nadmiarowych właściwościTypy słabe
Typ uznaje się za słaby, gdy zawiera wyłącznie zestaw właściwości opcjonalnych:
type X = { a?: string; b?: string;};TypeScript uznaje przypisanie czegokolwiek do typu słabego za błąd, jeśli nie występują żadne wspólne właściwości. Na przykład poniższy kod zgłasza błąd:
type Options = { a?: string; b?: string;};
const fn = (options: Options) => undefined;
fn({ c: 'c' }); // NiepoprawneChoć nie jest to zalecane, w razie potrzeby można pominąć tę kontrolę za pomocą asercji typu:
type Options = { a?: string; b?: string;};const fn = (options: Options) => undefined;fn({ c: 'c' } as Options); // PoprawneMożna też dodać unknown do sygnatury indeksowej typu słabego:
type Options = { [prop: string]: unknown; a?: string; b?: string;};
const fn = (options: Options) => undefined;fn({ c: 'c' }); // PoprawneŚcisła kontrola literałów obiektowych (świeżość)
Ścisła kontrola literałów obiektowych, czasami nazywana „świeżością”, jest funkcją TypeScript, która pomaga wykrywać nadmiarowe lub błędnie zapisane właściwości, które w przeciwnym razie pozostałyby niezauważone podczas zwykłej kontroli typów strukturalnych.
Podczas tworzenia literału obiektowego kompilator TypeScript uznaje go za „świeży”. Jeśli literał obiektowy zostanie przypisany do zmiennej lub przekazany jako parametr, TypeScript zgłosi błąd, jeśli literał ten określa właściwości, które nie istnieją w typie docelowym.
„Świeżość” znika jednak, gdy literał obiektowy zostanie poszerzony lub zostanie użyta asercja typu.
Oto kilka przykładów ilustrujących tę funkcję:
type X = { a: string };type Y = { a: string; b: string };
let x: X;x = { a: 'a', b: 'b' }; // Sprawdzanie świeżości: Niepoprawne przypisanievar y: Y;y = { a: 'a', bx: 'bx' }; // Sprawdzanie świeżości: Niepoprawne przypisanie
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Poszerzanie: Brak błędów, typy zgodne strukturalnie
fn({ a: 'a', bx: 'b' }); // Sprawdzanie świeżości: Niepoprawny argument
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Poszerzanie: Bez sprawdzania świeżościWnioskowanie typów
TypeScript może wnioskować typy, jeśli adnotacji nie podano podczas:
- Inicjalizacji zmiennej.
- Inicjalizacji elementu składowego.
- Ustawiania wartości domyślnych parametrów.
- Określania typu zwracanego przez funkcję.
Na przykład:
let x = 'x'; // Wywnioskowany typ to stringKompilator TypeScript analizuje wartość lub wyrażenie i określa jego typ na podstawie dostępnych informacji.
Bardziej zaawansowane wnioskowanie
Gdy we wnioskowaniu typu używanych jest wiele wyrażeń, TypeScript szuka „najlepszego wspólnego typu”. Na przykład:
let x = [1, 'x', 1, null]; // Wywnioskowany typ to: (string | number | null)[]Jeśli kompilator nie może znaleźć najlepszego wspólnego typu, zwraca typ unii. Na przykład:
let x = [new RegExp('x'), new Date()]; // Wywnioskowany typ to: (RegExp | Date)[]TypeScript wykorzystuje „typowanie kontekstowe” oparte na miejscu występowania zmiennej, aby wnioskować typy. W poniższym przykładzie kompilator wie, że e jest typu MouseEvent, ze względu na typ zdarzenia click zdefiniowany w pliku lib.d.ts, który zawiera deklaracje otoczenia dla różnych typowych konstrukcji JavaScript i DOM:
window.addEventListener('click', function (e) {}); // Wywnioskowany typ e to MouseEventPoszerzanie typów
Poszerzanie typów jest procesem, w którym TypeScript przypisuje typ do zmiennej zainicjalizowanej bez adnotacji typu. Pozwala przechodzić od węższych typów do szerszych, ale nie odwrotnie. W poniższym przykładzie:
let x = 'x'; // TypeScript wnioskuje string, czyli szeroki typlet y: 'y' | 'x' = 'y'; // Typ y jest unią typów literałowychy = x; // Niepoprawne: Typ 'string' nie może być przypisany do typu '"x" | "y"'.TypeScript przypisuje typ string do x na podstawie pojedynczej wartości podanej podczas inicjalizacji (x). Jest to przykład poszerzania.
TypeScript zapewnia sposoby kontrolowania procesu poszerzania, na przykład przez użycie „const”.
Const
Użycie słowa kluczowego const podczas deklarowania zmiennej powoduje, że TypeScript wnioskuje węższy typ.
Na przykład:
const x = 'x'; // TypeScript wnioskuje dla x typ 'x', czyli węższy typlet y: 'y' | 'x' = 'y';y = x; // Poprawne: Wywnioskowany typ x to 'x'Dzięki użyciu const do zadeklarowania zmiennej x jej typ zostaje zawężony do konkretnej wartości literałowej ‘x’. Ponieważ typ x jest zawężony, można przypisać go do zmiennej y bez żadnego błędu.
Typ może zostać wywnioskowany w ten sposób, ponieważ zmiennym const nie można ponownie przypisać wartości, a więc ich typ można zawęzić do konkretnego typu literałowego, w tym przypadku do typu literałowego ‘x’.
Modyfikator Const parametrów typu
Od wersji 5.0 w TypeScript można określić atrybut const dla parametru typu generycznego. Pozwala to wywnioskować możliwie najbardziej precyzyjny typ. Spójrzmy na przykład bez użycia const:
function identity<T>(value: T) { // Tutaj bez const return value;}const values = identity({ a: 'a', b: 'b' }); // Wywnioskowany typ to: { a: string; b: string; }Jak widać, właściwości a i b mają wywnioskowany typ string.
Zobaczmy teraz różnicę w wersji z const:
function identity<const T>(value: T) { // Użycie modyfikatora const dla parametrów typu return value;}const values = identity({ a: 'a', b: 'b' }); // Wywnioskowany typ to: { a: "a"; b: "b"; }Teraz widać, że właściwości a i b są wnioskowane jako literały łańcuchowe, a nie tylko jako typy string.
Asercja Const
Ta funkcja pozwala zadeklarować zmienną z bardziej precyzyjnym typem literałowym na podstawie jej wartości inicjalizacyjnej, wskazując kompilatorowi, że wartość powinna być traktowana jako niemutowalny literał. Oto kilka przykładów:
Dla pojedynczej właściwości:
const v = { x: 3 as const,};v.x = 3;Dla całego obiektu:
const v = { x: 1, y: 2,} as const;Może to być szczególnie przydatne podczas definiowania typu krotki:
const x = [1, 2, 3]; // number[]const y = [1, 2, 3] as const; // Krotka readonly [1, 2, 3]Jawna adnotacja typu
Możemy jawnie przekazać konkretny typ. W poniższym przykładzie właściwość x ma typ number:
const v = { x: 1, // Wywnioskowany typ: number (poszerzanie)};v.x = 3; // PoprawneAdnotację typu można doprecyzować za pomocą unii typów literałowych:
const v: { x: 1 | 2 | 3 } = { x: 1, // x jest teraz unią typów literałowych: 1 | 2 | 3};v.x = 3; // Poprawnev.x = 100; // NiepoprawneZawężanie typów
Zawężanie typów jest w TypeScript procesem, w którym typ ogólny zostaje zawężony do bardziej szczegółowego typu. Dzieje się tak, gdy TypeScript analizuje kod i ustala, że określone warunki lub operacje mogą doprecyzować informacje o typie.
Typy można zawężać na różne sposoby, w tym przez:
Warunki
Za pomocą instrukcji warunkowych, takich jak if lub switch, TypeScript może zawęzić typ na podstawie wyniku warunku. Na przykład:
let x: number | undefined = 10;
if (x !== undefined) { x += 100; // Typ to number, zawężony przez warunek}Zgłoszenie wyjątku lub zwrócenie wartości
Zgłoszenie błędu lub wcześniejsze zwrócenie wartości z gałęzi może sprawić, że TypeScript zawęzi typ. Na przykład:
let x: number | undefined = 10;
if (x === undefined) { throw 'error';}x += 100;Inne sposoby zawężania typów w TypeScript obejmują:
- Operator
instanceof: służy do sprawdzania, czy obiekt jest instancją określonej klasy. - Operator
in: służy do sprawdzania, czy właściwość istnieje w obiekcie. - Operator
typeof: służy do sprawdzania typu wartości w czasie wykonywania. - Wbudowane funkcje, takie jak
Array.isArray(): służą do sprawdzania, czy wartość jest tablicą.
Unia dyskryminowana
Użycie „unii dyskryminowanej” jest w TypeScript wzorcem, w którym do obiektów dodaje się jawny „znacznik”, aby rozróżnić różne typy w obrębie unii. Wzorzec ten jest również nazywany „unią znakowaną”. W poniższym przykładzie „znacznik” jest reprezentowany przez właściwość „type”:
type A = { type: 'type_a'; value: number };type B = { type: 'type_b'; value: string };
const x = (input: A | B): string | number => { switch (input.type) { case 'type_a': return input.value + 100; // typ to A case 'type_b': return input.value + 'extra'; // typ to B }};Typy strażnicze zdefiniowane przez użytkownika
W przypadkach, gdy TypeScript nie jest w stanie określić typu, można napisać funkcję pomocniczą znaną jako „typ strażniczy zdefiniowany przez użytkownika”. W poniższym przykładzie użyjemy predykatu typu, aby zawęzić typ po zastosowaniu określonego filtrowania:
const data = ['a', null, 'c', 'd', null, 'f'];
const r1 = data.filter(x => x != null); // Typ to (string | null)[], TypeScript nie zdołał poprawnie wywnioskować typu
const isValid = (item: string | null): item is string => item !== null; // Własny strażnik typu
const r2 = data.filter(isValid); // Typ jest teraz poprawny: string[], użycie predykatu typu pozwoliło zawęzić typZawężanie za pomocą switch-true
TypeScript 5.3 dodaje zawężanie za pomocą switch-true, które umożliwia zastąpienie nieczytelnych łańcuchów if/else konstrukcją switch (true) wykorzystującą warunki logiczne. Poprawia to czytelność, a jednocześnie nadal zawęża typy. Przypomina dopasowywanie wzorców, ale jest prostsze.
function classify(x: unknown) { switch (true) { case typeof x === 'string': return `"${x.toUpperCase()}"`; case typeof x === 'number': return x > 0 ? 'positive' : 'negative'; case Array.isArray(x): return `[${x.length} items]`; default: return 'something else'; }}