Advanced TypeScript

Literal Types

Většinou si vystačíme s takzvanými primitivními typy, které už dobře znáte - to je třeba string nebo number. Co když se ale stane, že potřebujeme pouze specifický string - tj. nechceme typ string, ale typ yellow? Použití hodnoty místo obyčejného typu se říká literal types a v základu je to velice jednoduché. Představme si, že vyrábíme aplikaci pro květinářství a potřebujeme si zadefinovat typ pro květiny:


type Flower = {
    name: 'rose' | 'tulip' | 'sunflower';
    color: 'yellow' | 'red' | 'pink';
    hasThorns: boolean;
}

Nemůžeme si jen tak vymyslet nový druh květiny nebo barvu, kterou naše květinářství nemá a nemůže prodávat. Kdyby name nebo color byly jen string, mohly by se nám do aplikace dostat vážné bugy. Když je ale zadefinujeme jako v příkladu nahoře, TypeScript na nás bude v kódu křičet, kdykoli bychom se pokusili definovat barvu/květinu, kterou jsme v typu nespecifikovali.

To těžší přichází ve chvíli, kdy květiny a barvy potřebujeme na více místech. Druh květin, které v našem květinářství prodáváme, potřebujeme ve vyhledávání, v hlavičce stránky, na detailu květiny pro doporučení ostatních... ale květiny v reálné aplikaci nebudeme mít pouze tři, takže bychom napříč aplikací museli pořád dokola v typech a props definovat třeba dvacet druhů květin. Nakonec by se nám někde stalo, že nám na jednom místě květiny chybí a na druhém přidáváme neexistující a nikdo by nevěděl, kde je vlastně pravda. Tady se můžete setkat s termínem SSOT - "Single Source Of Truth", jediný zdroj pravdy, a zrovna při vývoji určitě chceme vědět, co pro nás SSOT je a odkazovat se k němu místo duplikace definicí.


const flowers = ['rose', 'sunflower', 'tulip'];
const colors = ['yellow', 'red', 'pink', 'white'];

To už je lepší. Náš SSOT budou proměnné flowers a colors, které máme v aplikaci vypsané pouze na jednom místě. Jejich typ si TypeScript ale odvodí jako string[] a to nám nestačí. Aby si TypeScript jejich typ odvodil jako literal types, musíme mu trochu pomoct a to napsáním as const za naše proměnné.


const flowers = ['rose', 'sunflower', 'tulip'] as const;
const colors = ['yellow', 'red', 'pink', 'white'] as const;

Super! Když teď najedeme myškou na flowers, TypeScript už nám zobrazí, že jejich typ není string[], ale buď rose nebo sunflower nebo tulip. Teď už nám zbývá jenom tyhle typy použít v našem objektu a na to má TypeScript speciální syntaxi, kterou si budete muset zapamatovat.


type Name = (typeof flowers)[number];
type Color = (typeof colors)[number];

type Flower = {
    name: Name;
    color: Color;
    hasThorns: boolean;
}

typeof flowers říká "tohle bude typu flowers", ale flowers jsou pole, takže musíme specifikovat ještě [number].

[number] funguje jako index (proto je taky v hranatých závorkách) a říká "tohle platí pro všechny jednotlivé možnosti tohohle pole".

Keyof Type Operator

keyof operátor nám pomůže získat typ z klíčů objektu.


const fontWeights = {
    normal: '400',
    semibold: '600',
    medium: '500',
    bold: '700',
    extrabold: '800',
};

type FontWeight = keyof typeof fontWeights;

FontWeight type je teď 'normal' | 'semibold' | 'medium' | 'bold' | 'extrabold'.

Kdy se nám něco takového může hodit? Podle výše zmíněného příkladu třeba když budeme chtít uživateli umožnit změnit v jeho blog postu font weight.


const [fontWeight, setFontWeight] = useState<FontWeight>('normal')

function changeFontWeight(weight: FontWeight): void {
    setFontWeight(weight);
}

Test

Je veškerý validní javascriptový kód zároveň validním typescriptovým kódem?

Ano! TypeScript je tzv. "superset" JavaScriptu. To znamená, že obsahuje a umí totéž, co JavaScript, jenom ho rozšiřuje o typy. To nám hodně ulehčuje přechod na TS ve velkých projektech - náš projekt může kombinovat .js a .ts soubory.

Co znamená otazník v typu { name?: string; }

Property name je nepovinné.

Jaký je rozdíl mezi null a undefined?

Představte si, že máte knihovnu, ve které nejsou žádné knížky, protože jste tam žádné nedali.

Pokud jste na to jenom zapomněli, value knihovny by byla undefined. Knihovna existuje (proměnná je deklarovaná), ale nic v ní není, protože tam nikdo nic nedal.

Pokud jste do knihovny ale nic nedali schválně, víte, že tam nic nemá být, můžete jí přiřadit hodnotu naznačující "prázdnost" - null.

Null musí někdo přiřadit, undefined je defaultně vše, co nemá nic přiřazené.

Co pro TypeScript znamená, když za proměnnou napíšu as const?

Typ proměnné se bude řídit literal types, místo např. string[] bude přijímat pouze specifické stringy, které jsou v proměnné uložené.

Co je to type inference?

TypeScript si spoustu typů odvodí sám i bez naší pomoci. Pokud do proměnné vložíme string "hello", nemusíme specifikovat, že je to proměnná typu string. Stejně tak pokud třeba useState("hello") rovnou dostane "hello" jako initial hodnotu, TypeScript už ví, že do něj může nastavit pouze string. Pokud by ale initial hodnota byla prázdná useState(null), TypeScript by si myslel, že žádnou jinou hodnotu než null state mít nemůže a musíme ho otypovat sami jako useState<string | null>(null).

Proč by TypeScript měl být devDependency?

devDependencies, tj. development dependencies, obsahují všechno, co potřebujete pouze při vývoji, ne do produkce/při nasazení (deploy) svojí stránky. Cokoli je tak označené, to např. Vite může při zpracování vašeho kódu přeskočit. TypeScript sem patří proto, že aby mohl běžet v JavaScriptovém prostředí (třeba prohlížeč), musí být zkompilovaný (přepsaný) do JavaScriptu. Vy jste se s tím nesetkaly, protože za vás kompilaci na pozadí řeší React při každém npm start nebo npm build.

Lze vytvořit nový typ ze dvou již existujících typů?


interface NewType extends OldType {
  newTypeProperty1: string;
  newTypeProperty2: string;
}
nebo

  type NewType = OldType & {
    newTypeProperty1: string;
    newTypeProperty2: string;
  }

Můžeme TypeScriptu "lhát" a zpětně ho přesvědčit, že je nějaké proměnná jiného typu než si myslí?

Bohužel můžeme. Stačí za proměnnou přidat as NewType. Ideálně bychom to dělat neměli, protože pak ztrácí TypeScript smysl, ale může se stát, že si potřebujeme jen něco otestovat a tohle nám může pomoct. Kde se nám ještě může hodit je pokud od nepořádného kolegy dostaneme nějaké ošklivé any, ale my víme, že to je string a potřebujeme zjistit jeho délku, as string by nám pomohlo (a pak šup to any opravit!).

Generics

Generiku už jsme si jednou ukazovali, pojďme se na ni přesto podívat na jednodušším příkladu a vysvětlit, kdy ji můžeme potřebovat. Možná už jste slyšeli o principu DRY (tj. "Neopakujme se.", "Don't repeat yourself."), který nám s tím pomůže. Jedním ze způsobů jak se vyhnout zbytečnému opakování je totiž právě generika.

Představte si, že programujete web pro svůj oblíbený časopis a často potřebujete potřebujete zvýraznit první element pole - jednou je to první článek na blogu, podruhé první písmeno článku a tak dále. Funkce na získání prvního elementu pole bude vypadat pokaždé stejně, liší se jenom pole, která jí předáváte. Jednou to můžou být čísla, jindy stringy. Mohli bychom napsat:

const getFirstLetter = (allLetters: string[]) => allLetters[0];

const getFirstBlogPost = (posts: BlogPost[]) => posts[0];

const getFirstNumber = (numbers: number[]) => numbers[0];

Hmmm...

Nezdá se vám to jako hodně opakování? V případě takhle krátké a jasné funkce to nemusí vadit, ale úplně stejný princip bude platit i s komponenty, které v Reactu budete chtít vyrábět maximálně znovupoužitelné. Stejně tak nebudete chtít psát logiku useForm hooku, který jsme si ukazovali minule, pokaždé znovu jen proto, že váš formulář zpracovává jiná data.

S použitím generiky můžeme naše tři funkce na získání prvního prvku pole spojit do jedné:

const getFirstItem = <T>(item: T[]): T => item[0];

getFirstItem([1, 2, 3]) nám vrátí číslo 1. getFirstItem(['a', 'b', 'c']) nám vrátí a. A my už se nemusíme opakovat. :)

Utility Types

TypeScript nám poskytuje takzvané "utility types". Nemusíme je odnikud importovat, stačí rovnou použít, a slouží nám k vyjádření opakující se logiky.

Potřebujete vytvořit typ, který vypadá úplně stejně jako jiný, ale má nějaké property navíc? Potřebujete zduplikovat existující typ, ale všechny jeho properties jsou nepovinné? Tak to už je pro vás připravené. Fungují na principu generiky, což už poznáte podle < > zobáčků. To proto, že budou fungovat stejně pro jakýkoli typ, který jim předáme.

Cvičení

Pokud nám zbyde čas na cvičení, byli bychom rádi, kdybyste nevytvářeli nový projekt, ale sáhly po tom posledním, který máte rozdělaný - ten, který máte nejvíc v paměti. Samy si tak můžete vybrat koncept z dnešní lekce, který vám byl nejvíc sympatický, a zkusit ho implementovat.

Může to být třeba: