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 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);
}
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.
{ name?: string; }
Property name je nepovinné.
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é.
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é.
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).
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.
interface NewType extends OldType {
newTypeProperty1: string;
newTypeProperty2: string;
}
nebo
type NewType = OldType & {
newTypeProperty1: string;
newTypeProperty2: string;
}
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!).
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. :)
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.
Pick<Type, Keys> nám umožňuje vytvořit nový typ
z properties jiného. Představte si, že má váš User v
typu properties jako
{ hobbies: Hobby[]; favoriteAnimal: 'dog' | 'cat'
}. Až budete chtít třeba favoriteAnimal použít na
vypsání těchto properties v nějaké komponentě nebo předáním v
props, budete muset znovu specifikovat všechny možnosti. A pak se
taky může lehce stát, že na jednom místě může být
favoriteAnimal jenom pes nebo kočka, na jiném ale i
papoušek, kterému se pak nezobrazí ikonka, protože o něm polovina
typů neví.
Abyste se tomu vyhnuli, můžete si props komponenty
FavoriteAnimal vytvořit přímo z typu uživatele.
type User = {
name: string;
age: number
hobbies: Hobby[];
favoriteAnimal: 'dog' | 'cat' | 'parrot';
}
type Props = Pick<User, 'favoriteAnimal'>;
export const FavoriteAnimal: FC<Props> = (props) => <div>{props.favoriteAnimal}</div>;
Omit<Type, Keys> nám pomáhá vytvořit nový typ z
již existujícího stejně jako Pick, ale navíc s
možností vynechat properties, které nepotřebujeme.
Zůstaňme u předchozího příkladu. Komponenta
FavoriteAnimal je hotová, property
favoriteAnimal už tedy pro zobrazení zbytku informací
o našem uživateli nepotřebujeme. Omit nám umožní
tohle zadefinovat jediným řádkem.
type Props = Omit<User, 'favoriteAnimal'>;
export const UserInfo: FC<Props> = ({ name, age, hobbies }) => ...;
Partial<Type> nemusí mít na první pohled jasné
využití. Zní, jako že si vybereme jen "part" (část) typu, ale na
to přece máme Pick. Abychom mu porozuměli, musíme si
nejdřív zopakovat, co v typu znamená ?. Otazník
označuje nepovinnost. Například z typu User, který je
{ name: string; age?: number }, tak můžeme
na první pohled odvodit, že vždycky musí mít jméno, ale jen může
(nebo nemusí) mít definovaný věk. A co tedy dělá
Partial? Jednoduše všem properties přidá
?. Jakýkoli typ, který mu genericky předáme, tak
obsahuje jenom nepovinné properties.
A příklad použití v praxi? Představte si, že máte vyrobit formulář, ve kterém si uživatel mění svoje kontaktní údaje. Někdy si vyplní telefon, někdy e-mail, někdy adresu. Formulář ale může odeslat i kdyby vyplnil jen jednu z daných možností, tedy potřebuje typ, kde budou všechny možné properties uživatele, ale všechny nepovinné.
interface User {
name: string;
email: string;
phone?: string;
address?: string;
}
function updateUserDetails(user: User, details: Partial<User>): User {
return { ...user, ...details };
}
Record<Key, Type> za nás vytváří typ pro
objekt, o kterém víme, že všechny jeho keys budou stejného typu a
všechny jeho values budou stejného typu.
Co si pod tím představit? Děláme aplikaci pro květinářství a uživatelé si chtějí vybírat podle barev. Vytvoříme si (nebo dostaneme z backendu) seznam barev a seznam květin.
const flowers = ['rose', 'sunflower', 'tulip'] as const;
const colors = ['yellow', 'red', 'pink', 'white'] as const;
type Name = (typeof flowers)[number];
type Color = (typeof colors)[number];
Spojit je dohromady je jednoduché a máme mnoho možností, pro účely našeho příkladu to bude následující objekt:
const flowerColors = {
yellow: ['sunflower'],
red: ['rose', 'tulip'],
pink: ['rose', 'tulip'],
white: ['rose'],
};
Čeho si můžeme všimnout je, že klíč našeho objektu je vždycky
color a value je pole flowers. Record nám umožní
tohle shrnout do jednoho typu.
type FlowerColors = Record<Color, Name[]>;
Poslední utility type, který si představíme, je
Readonly<Type> pro objekty a
ReadonlyArray<Type> pro pole.
Tady nás nečeká žádný chyták "read only" (pouze ke čtení) opravdu znamená, že typ je pouze ke čtení.
V Reactu je populární funkcionální přístup. Pod něj patří
třeba immutability, se kterou jste se v tomhle kurzu už
potkali a umíte do pole přidávat prvky takhle
[...previous, new]
místo .push metody. Problém je, že JavaScript nám
dovoluje občas dělat dost ošklivé věci. I na pole definované ne
jako let, ale jako const, můžeme použít
metodu .push a přidat tak do něj další prvek.
Readonly typ je taková bariéra, kterou si sami můžeme postavit,
abychom měli jistotu, že se do našeho pole nebo objektu opravdu
nedostane žádná nechtěnná změna - TypeScript by okamžitě křičel.
const arrayOfNumbers: ReadonlyArray<number> = [1, 2, 3]
type Props = Readonly<{
name: string;
age: number;
}>;
Tyhle dva utility typy pravděpodobně samy od sebe moc používate nebudete, ale měly bystě vědět, co znamenají, když na ně v budoucí práci narazíte.
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:
<T>
string[] nebo number[]
Pick nebo Omit utility type při
posílání props podobných nějakému objektu
Record utility typu