Zkoumání typového systému
Jazyková služba TypeScriptu
Jazyková služba TypeScriptu, známá také jako tsserver, nabízí různé funkce, například hlášení chyb, diagnostiku, kompilaci při uložení, přejmenování, přechod na definici, seznamy návrhů pro doplňování, nápovědu k signaturám a další. Používají ji především integrovaná vývojová prostředí (IDE) k poskytování podpory IntelliSense. Bezproblémově se integruje s Visual Studio Code a využívají ji nástroje jako Conquer of Completion (Coc).
Vývojáři mohou využít vyhrazené API a vytvářet vlastní pluginy jazykové služby pro zlepšení práce s kódem TypeScriptu. To může být obzvláště užitečné při implementaci speciálních funkcí lintu nebo při zajištění automatického doplňování pro vlastní šablonovací jazyk.
Příkladem vlastního pluginu používaného v praxi je „typescript-styled-plugin“, který poskytuje hlášení syntaktických chyb a podporu IntelliSense pro vlastnosti CSS ve styled components.
Další informace a úvodní návody najdete na oficiální wiki TypeScriptu na GitHubu: https://github.com/microsoft/TypeScript/wiki/
Strukturální typování
TypeScript je založen na strukturálním typovém systému. To znamená, že kompatibilita a ekvivalence typů se určují podle skutečné struktury nebo definice typu, nikoli podle jeho názvu či místa deklarace, jako je tomu v nominálních typových systémech, například v C# nebo C.
Strukturální typový systém TypeScriptu byl navržen podle toho, jak za běhu funguje dynamický systém duck typingu v JavaScriptu.
Následující příklad je platný kód TypeScriptu. Jak vidíte, „X“ a „Y“ mají stejný člen „a“, přestože jsou deklarovány pod různými názvy. Typy se určují podle svých struktur a v tomto případě jsou struktury stejné, takže jsou kompatibilní a kód je platný.
type X = { a: string;};type Y = { a: string;};const x: X = { a: 'a' };const y: Y = x; // ValidZákladní pravidla porovnávání v TypeScriptu
Proces porovnávání v TypeScriptu je rekurzivní a provádí se na typech vnořených na libovolné úrovni.
Typ „X“ je kompatibilní s „Y“, pokud má „Y“ alespoň stejné členy jako „X“.
type X = { a: string;};const y = { a: 'A', b: 'B' }; // Valid, as it has at least the same members as Xconst r: X = y;Parametry funkcí se porovnávají podle typů, nikoli podle názvů:
type X = (a: number) => void;type Y = (a: number) => void;let x: X = (j: number) => undefined;let y: Y = (k: number) => undefined;y = x; // Validx = y; // ValidNávratové typy funkcí musí být stejné:
type X = (a: number) => undefined;type Y = (a: number) => number;let x: X = (a: number) => undefined;let y: Y = (a: number) => 1;y = x; // Invalidx = y; // InvalidNávratový typ zdrojové funkce musí být podtypem návratového typu cílové funkce:
let x = () => ({ a: 'A' });let y = () => ({ a: 'A', b: 'B' });x = y; // Validy = x; // Invalid member b is missingVynechání parametrů funkce je povoleno, protože jde o běžnou praxi v JavaScriptu, například při použití „Array.prototype.map()“:
[1, 2, 3].map((element, _index, _array) => element + 'x');Proto jsou následující deklarace typů zcela platné:
type X = (a: number) => undefined;type Y = (a: number, b: number) => undefined;let x: X = (a: number) => undefined;let y: Y = (a: number) => undefined; // Missing b parametery = x; // ValidJakékoli další volitelné parametry zdrojového typu jsou platné:
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; // Validx = y; //ValidJakékoli volitelné parametry cílového typu bez odpovídajících parametrů ve zdrojovém typu jsou platné a nepředstavují chybu:
type X = (a: number) => undefined;type Y = (a: number, b?: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Validx = y; // ValidSe zbytkovým parametrem se zachází jako s nekonečnou řadou volitelných parametrů:
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //validFunkce s přetížením jsou platné, pokud je signatura přetížení kompatibilní se signaturou implementace:
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'); // Validx('a', 1); // Valid
function y(a: string): void; // Invalid, not compatible with implementation signaturefunction y(a: string, b: number): void;function y(a: string, b: number): void { console.log(a, b);}y('a');y('a', 1);Porovnání parametrů funkcí uspěje, pokud jsou zdrojové a cílové parametry přiřaditelné nadtypům nebo podtypům (bivariance).
// Supertypeclass X { a: string; constructor(value: string) { this.a = value; }}// Subtypeclass Y extends X {}// Subtypeclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// Bivariance does accept supertypesconsole.log(getA(new X('x'))); // Validconsole.log(getA(new Y('Y'))); // Validconsole.log(getA(new Z('z'))); // ValidVýčtové typy lze porovnávat s čísly a naopak a takové porovnání je platné, ale porovnání hodnot různých výčtových typů je neplatné.
enum X { A, B,}enum Y { A, B, C,}const xa: number = X.A; // Validconst ya: Y = 0; // ValidX.A === Y.A; // InvalidU instancí třídy se kontroluje kompatibilita jejich soukromých a chráněných členů:
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'); // InvalidKontrola při porovnávání nezohledňuje odlišnou hierarchii dědičnosti, například:
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; // Validx === z; // Valid even if z is from a different inheritance hierarchyGenerické typy se porovnávají podle struktury výsledného typu po uplatnění generického parametru; porovnává se pouze konečný výsledek jako negenerický typ.
interface X<T> { a: T;}let x: X<number> = { a: 1 };let y: X<string> = { a: 'a' };x === y; // Invalid as the type argument is used in the final structureinterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Valid as the type argument is not used in the final structurePokud generické typy nemají určený typový argument, se všemi neurčenými argumenty se zachází jako s typem „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; // ValidPamatujte:
let a: number = 1;let b: number = 2;a = b; // Valid, everything is assignable to itself
let c: any;c = 1; // Valid, all types are assignable to any
let d: unknown;d = 1; // Valid, all types are assignable to unknown
let e: unknown;let e1: unknown = e; // Valid, unknown is only assignable to itself and anylet e2: any = e; // Validlet e3: number = e; // Invalid
let f: never;f = 1; // Invalid, nothing is assignable to never
let g: void;let g1: any;g = 1; // Invalid, void is not assignable to or from anything except anyg = g1; // ValidPoznamenejme, že při zapnuté volbě „strictNullChecks“ se s „null“ a „undefined“ zachází podobně jako s „void“; jinak jsou podobné typu „never“.
Typy jako množiny
V TypeScriptu je typ množinou možných hodnot. Tato množina se také označuje jako obor hodnot typu. Každou hodnotu typu lze chápat jako prvek množiny. Typ stanovuje omezení, která musí každý prvek množiny splňovat, aby byl považován za jejího člena. Hlavním úkolem TypeScriptu je kontrolovat a ověřovat, zda je jedna množina podmnožinou druhé.
TypeScript podporuje různé druhy množin:
| Pojem z teorie množin | TypeScript | Poznámky |
|---|---|---|
| Prázdná množina | never | „never“ obsahuje cokoli kromě sebe sama |
| Jednoprvková množina | undefined / null / literálový typ | |
| Konečná množina | boolean / sjednocení | |
| Nekonečná množina | string / number / object | |
| Univerzální množina | any / unknown | Každý prvek je členem „any“ a každá množina je jeho podmnožinou / „unknown“ je typově bezpečným protějškem „any“ |
Zde je několik příkladů:
| TypeScript | Pojem z teorie množin | Příklad |
|---|---|---|
| never | ∅ (prázdná množina) | const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’ |
| Literálový typ | Jednoprvková množina | type X = ‘X’; |
| type Y = 7; | ||
| Hodnota přiřaditelná T | Hodnota ∈ T (je prvkem) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| T1 přiřaditelný T2 | T1 ⊆ T2 (je podmnožinou) | 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 (je podmnožinou) | type X = ‘X’ extends string ? true : false; |
| T1 | T2 | T1 ∪ T2 (sjednocení) | type XY = ‘X’ | ‘Y’; |
| type JK = 1 | 2; | ||
| T1 & T2 | T1 ∩ T2 (průnik) | type X = { a: string } |
| type Y = { b: string } | ||
| type XY = X & Y | ||
| const x: XY = { a: ‘a’, b: ‘b’ } | ||
| unknown | Univerzální množina | const x: unknown = 1 |
Sjednocení (T1 | T2) vytváří širší množinu (obě množiny):
type X = { a: string;};type Y = { b: string;};type XY = X | Y;const r: XY = { a: 'a', b: 'x' }; // ValidPrůnik (T1 & T2) vytváří užší množinu (pouze společné prvky):
type X = { a: string;};type Y = { a: string; b: string;};type XY = X & Y;const r: XY = { a: 'a' }; // Invalidconst j: XY = { a: 'a', b: 'b' }; // ValidKlíčové slovo extends lze v tomto kontextu chápat jako „je podmnožinou“. Nastavuje omezení pro typ. Při použití s generickým typem omezuje extends generický typový parametr na konkrétnější typ.
Poznamenejme, že extends zde nijak nesouvisí s dědičností tříd ve smyslu OOP.
TypeScript pracuje se strukturálními typy a nemá přísnou nominální hierarchii. Ve skutečnosti se, stejně jako v příkladu níže, mohou dva typy překrývat, aniž by byl jeden podtypem druhého, protože TypeScript zohledňuje strukturu neboli tvar objektů.
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; // ValidPřiřazení typu: deklarace typů a přetypování
V TypeScriptu lze typ přiřadit různými způsoby:
Deklarace typu
V následujícím příkladu používáme x: X („: Type“) k deklaraci typu proměnné x.
type X = { a: string;};
// Type declarationconst x: X = { a: 'a',};Pokud proměnná neodpovídá určenému formátu, TypeScript ohlásí chybu. Například:
type X = { a: string;};
const x: X = { a: 'a', b: 'b', // Error: Object literal may only specify known properties};Přetypování
Přetypování lze přidat pomocí klíčového slova as. Tím sdělujeme kompilátoru, že vývojář má o typu více informací, a potlačujeme případné chyby.
Například:
type X = { a: string;};const x = { a: 'a', b: 'b',} as X;V uvedeném příkladu je objekt x pomocí klíčového slova as přetypován na typ X. To kompilátoru TypeScriptu sděluje, že objekt odpovídá určenému typu, přestože má navíc vlastnost b, která v definici typu není.
Přetypování je užitečné v situacích, kdy je nutné určit konkrétnější typ, zejména při práci s DOM. Například:
const myInput = document.getElementById('my_input') as HTMLInputElement;Zde přetypování as HTMLInputElement sděluje TypeScriptu, že s výsledkem getElementById se má zacházet jako s HTMLInputElement. Přetypování lze použít také k přemapování klíčů, jak ukazuje příklad níže se šablonovými literály:
type J<Type> = { [Property in keyof Type as `prefix_${string & Property}`]: () => Type[Property];};type X = { a: string; b: number;};type Y = J<X>;V tomto příkladu používá typ J<Type> mapovaný typ se šablonovým literálem k přemapování klíčů Type. Vytváří nové vlastnosti s „prefix_“ přidaným ke každému klíči a jejich odpovídajícími hodnotami jsou funkce vracející původní hodnoty vlastností.
Při použití přetypování TypeScript neprovádí kontrolu nadbytečných vlastností. Proto je obecně vhodnější použít deklaraci typu, pokud je struktura objektu známa předem.
Ambientní deklarace
Ambientní deklarace jsou soubory popisující typy kódu JavaScriptu a jejich názvy mají formát .d.ts.. Obvykle se importují a používají k anotaci existujících knihoven JavaScriptu nebo k přidání typů do stávajících souborů JS ve vašem projektu.
Typy mnoha běžných knihoven najdete na: https://github.com/DefinitelyTyped/DefinitelyTyped/
a lze je nainstalovat pomocí:
npm install --save-dev @types/library-nameVámi definované ambientní deklarace můžete importovat pomocí odkazu se třemi lomítky:
/// <reference path="./library-types.d.ts" />Ambientní deklarace můžete používat i v souborech JavaScriptu pomocí // @ts-check.
Klíčové slovo declare umožňuje definovat typy pro existující kód JavaScriptu bez jeho importu a slouží jako zástupný zápis pro typy z jiného souboru nebo z globálního prostoru.
Kontrola vlastností a kontrola nadbytečných vlastností
TypeScript je založen na strukturálním typovém systému, ale kontrola nadbytečných vlastností je jeho funkcí, která umožňuje ověřit, zda má objekt přesně ty vlastnosti, které jsou uvedeny v typu.
Kontrola nadbytečných vlastností se provádí například při přiřazování objektových literálů proměnným nebo při jejich předávání jako argumentů funkci s nadbytečnou vlastností.
type X = { a: string;};const y = { a: 'a', b: 'b' };const x: X = y; // Valid because structural typingconst w: X = { a: 'a', b: 'b' }; // Invalid because excess property checkingSlabé typy
Typ je považován za slabý, pokud obsahuje pouze sadu vlastností, z nichž jsou všechny volitelné:
type X = { a?: string; b?: string;};TypeScript považuje za chybu přiřazení čehokoli slabému typu, pokud neexistuje žádný překryv; například následující kód ohlásí chybu:
type Options = { a?: string; b?: string;};
const fn = (options: Options) => undefined;
fn({ c: 'c' }); // InvalidAčkoli se to nedoporučuje, v případě potřeby lze tuto kontrolu obejít pomocí přetypování:
type Options = { a?: string; b?: string;};const fn = (options: Options) => undefined;fn({ c: 'c' } as Options); // ValidNebo přidáním unknown do indexové signatury slabého typu:
type Options = { [prop: string]: unknown; a?: string; b?: string;};
const fn = (options: Options) => undefined;fn({ c: 'c' }); // ValidPřísná kontrola objektových literálů (čerstvost)
Přísná kontrola objektových literálů, někdy označovaná jako „čerstvost“, je funkce TypeScriptu, která pomáhá zachytit nadbytečné vlastnosti nebo překlepy v jejich názvech, jež by při běžných kontrolách strukturálních typů zůstaly nepovšimnuty.
Při vytvoření objektového literálu jej kompilátor TypeScriptu považuje za „čerstvý“. Pokud je objektový literál přiřazen proměnné nebo předán jako parametr, TypeScript ohlásí chybu, jestliže objektový literál uvádí vlastnosti, které v cílovém typu neexistují.
„Čerstvost“ však zaniká, když je objektový literál rozšířen nebo je použito přetypování.
Zde je několik příkladů pro ilustraci:
type X = { a: string };type Y = { a: string; b: string };
let x: X;x = { a: 'a', b: 'b' }; // Freshness check: Invalid assignmentvar y: Y;y = { a: 'a', bx: 'bx' }; // Freshness check: Invalid assignment
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Widening: No errors, structurally type compatible
fn({ a: 'a', bx: 'b' }); // Freshness check: Invalid argument
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Widening: No Freshness checkOdvozování typů
Pokud není uvedena anotace, TypeScript dokáže odvodit typy při:
- Inicializaci proměnných.
- Inicializaci členů.
- Nastavování výchozích hodnot parametrů.
- Určování návratového typu funkce.
Například:
let x = 'x'; // The type inferred is stringKompilátor TypeScriptu analyzuje hodnotu nebo výraz a určuje jejich typ na základě dostupných informací.
Pokročilejší odvozování typů
Pokud se při odvozování typů používá více výrazů, TypeScript hledá „nejlepší společné typy“. Například:
let x = [1, 'x', 1, null]; // The type inferred is: (string | number | null)[]Pokud kompilátor nemůže najít nejlepší společné typy, vrátí sjednocení typů. Například:
let x = [new RegExp('x'), new Date()]; // Type inferred is: (RegExp | Date)[]TypeScript k odvozování typů využívá „kontextové typování“ podle umístění proměnné. V následujícím příkladu kompilátor ví, že e je typu MouseEvent, díky typu události click definovanému v souboru lib.d.ts, který obsahuje ambientní deklarace různých běžných konstrukcí JavaScriptu a DOM:
window.addEventListener('click', function (e) {}); // The inferred type of e is MouseEventRozšiřování typů
Rozšiřování typů je proces, při kterém TypeScript přiřazuje typ proměnné inicializované bez typové anotace. Umožňuje přechod od užších typů k širším, ale nikoli naopak. V následujícím příkladu:
let x = 'x'; // TypeScript infers as string, a wide typelet y: 'y' | 'x' = 'y'; // y types is a union of literal typesy = x; // Invalid Type 'string' is not assignable to type '"x" | "y"'.TypeScript přiřazuje string proměnné x na základě jediné hodnoty uvedené při inicializaci (x); jde o příklad rozšiřování.
TypeScript nabízí způsoby, jak proces rozšiřování řídit, například pomocí „const“.
Const
Použití klíčového slova const při deklaraci proměnné vede v TypeScriptu k odvození užšího typu.
Například:
const x = 'x'; // TypeScript infers the type of x as 'x', a narrower typelet y: 'y' | 'x' = 'y';y = x; // Valid: The type of x is inferred as 'x'Použitím const k deklaraci proměnné x se její typ zúží na konkrétní literálovou hodnotu ‘x’. Protože je typ x zúžen, lze ji bez chyby přiřadit proměnné y.
Typ lze odvodit proto, že proměnným const nelze znovu přiřazovat hodnoty, takže jejich typ lze zúžit na konkrétní literálový typ, v tomto případě na literálový typ ‘x’.
Modifikátor const u typových parametrů
Od verze TypeScriptu 5.0 lze u generického typového parametru uvést atribut const. To umožňuje odvodit nejpřesnější možný typ. Podívejme se na příklad bez použití const:
function identity<T>(value: T) { // No const here return value;}const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: string; b: string; }Jak vidíte, u vlastností a a b je odvozen typ string.
Nyní se podívejme na rozdíl oproti verzi s const:
function identity<const T>(value: T) { // Using const modifier on type parameters return value;}const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: "a"; b: "b"; }Nyní vidíme, že vlastnosti a a b jsou odvozeny jako řetězcové literály, nikoli pouze jako typy string.
Přetypování const
Tato funkce umožňuje deklarovat proměnnou s přesnějším literálovým typem na základě její inicializační hodnoty a sdělit tak kompilátoru, že s hodnotou má zacházet jako s neměnným literálem. Zde je několik příkladů:
U jedné vlastnosti:
const v = { x: 3 as const,};v.x = 3;U celého objektu:
const v = { x: 1, y: 2,} as const;To může být obzvláště užitečné při definování typu n-tice:
const x = [1, 2, 3]; // number[]const y = [1, 2, 3] as const; // Tuple of readonly [1, 2, 3]Explicitní typová anotace
Můžeme být konkrétní a uvést typ. V následujícím příkladu je vlastnost x typu number:
const v = { x: 1, // Inferred type: number (widening)};v.x = 3; // ValidTypovou anotaci můžeme zpřesnit pomocí sjednocení literálových typů:
const v: { x: 1 | 2 | 3 } = { x: 1, // x is now a union of literal types: 1 | 2 | 3};v.x = 3; // Validv.x = 100; // InvalidZužování typů
Zužování typů je proces v TypeScriptu, při kterém se obecný typ zužuje na konkrétnější. Dochází k tomu, když TypeScript analyzuje kód a určí, že určité podmínky nebo operace mohou zpřesnit informace o typu.
K zužování typů může docházet různými způsoby, mezi něž patří:
Podmínky
Pomocí podmíněných příkazů, jako je if nebo switch, může TypeScript zúžit typ na základě výsledku podmínky. Například:
let x: number | undefined = 10;
if (x !== undefined) { x += 100; // The type is number, which had been narrowed by the condition}Vyhození chyby nebo návrat
Vyhození chyby nebo předčasný návrat z větve lze využít k tomu, aby TypeScript snáze zúžil typ. Například:
let x: number | undefined = 10;
if (x === undefined) { throw 'error';}x += 100;Mezi další způsoby zužování typů v TypeScriptu patří:
- Operátor
instanceof: Slouží ke kontrole, zda je objekt instancí konkrétní třídy. - Operátor
in: Slouží ke kontrole, zda v objektu existuje vlastnost. - Operátor
typeof: Slouží ke kontrole typu hodnoty za běhu. - Vestavěné funkce jako
Array.isArray(): Slouží ke kontrole, zda je hodnota pole.
Diskriminované sjednocení
„Diskriminované sjednocení“ je vzor v TypeScriptu, při kterém se k objektům přidává explicitní „značka“ pro rozlišení jednotlivých typů ve sjednocení. Tento vzor se také označuje jako „označené sjednocení“. V následujícím příkladu je „značka“ reprezentována vlastností „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; // type is A case 'type_b': return input.value + 'extra'; // type is B }};Uživatelsky definovaní typoví strážci
V případech, kdy TypeScript nedokáže určit typ, lze napsat pomocnou funkci známou jako „uživatelsky definovaný typový strážce“. V následujícím příkladu použijeme typový predikát ke zúžení typu po provedení určitého filtrování:
const data = ['a', null, 'c', 'd', null, 'f'];
const r1 = data.filter(x => x != null); // The type is (string | null)[], TypeScript was not able to infer the type properly
const isValid = (item: string | null): item is string => item !== null; // Custom type guard
const r2 = data.filter(isValid); // The type is fine now string[], by using the predicate type guard we were able to narrow the typeZužování pomocí switch-true
TypeScript 5.3 přidává zužování pomocí switch-true, které umožňuje nahradit nepřehledné řetězce if/else konstrukcí switch (true) s logickými podmínkami. Zlepšuje čitelnost a přitom stále zužuje typy. Podobá se porovnávání vzorů, ale je jednodušší.
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'; }}