TypeScript
TypeScript

Základní typy v Typescriptu 2

Pavel  JaklPavel Jakl,

V našem předchozím článku jsme se seznámili se některými základními typy v TypeScriptu, které jsou základem pro jakoukoliv aplikaci. V tomto díle se podíváme dále na běžné typy TypeScriptu a prozkoumáme další typové konstrukty, které tuto technologii činí tak silnou a oblíbenou mezi vývojáři.

Začneme s 'type aliasy', což jsou vlastně 'přezdívky' pro typy, umožňující nám vytvořit srozumitelnější a čistší kód. Následně přejdeme k 'interface', klíčovému konceptu v TypeScriptu, který nám umožní definovat strukturu objektů a tříd. Ukážeme si, jak mohou interface zlepšit opakovanou použitelnost a údržbu kódu.

Dále se zaměříme na 'enumy', neboli výčtové typy, které nám umožňují definovat soubor pojmenovaných konstant. Vysvětlíme, jak enumy mohou zlepšit čitelnost kódu, usnadnit práci s pevně definovanými hodnotami a také se podíváme na úlohu typů null a undefined.

Type Aliases

Když jsme si povídali o objektových typech nebo o typech union, tak jsme je zapisovali přímo do parametru funkce nebo jako její návratovou hodnotu. Není to špatně, ale pokud budeme mít případ, že stejný typ bude využitý i v dalších funkcích, musíme ho tedy znovu napsat a při jakékoli změně budeme muset upravit všechny výskyty tohoto typu, což není zcela optimální. A k tomuto případu nám slouží právě type aliases. Vytvoříme jeden typ, na který budeme odkazovat jedním jménem. 

Vytvoříme tedy funkci na výpočet obsahu obdélníku. Do funkce musíme tedy zadat jeho strany. Vytvoříme nový type alias klíčovým slovem type a specifikujeme jeho strukturu. 

type Rectangle = {
	l: number
	w: number
}

function rectangleArea(rc: Rectangle){
	const area = rc.l * rc.w
	console.log("The rectangle area is " + area)
}

Type alias je možné vytvořit pro jakýkoli typ, ne jen pro objekty. Například pokud bychom chtěli vytvořit alias pro union type nebo pro pole, můžeme to udělat takto:

type ID = string | number
type array = Array<string> | string

Zde je důležité pochopit, že type aliasy nevytvářejí nové typy, jsou to jen jiné názvy pro stávající typy. TS ho interpretuje přesně jako původní typ, který tvoří type alias. V kódu je možné ho využít, jako by to byl samostatný typ, ale ve skutečnosti je to jen jiný název pro již existující datový typ.

Ukážeme si nějaký příklad. Definujeme si dva type aliasy. Oba budou mít datový typ string. 

type Name = string
type Message = string

function sayHi(name: Name): Message {
	return ("Hi " + name)
}

Může se zdát, že máme dva odlišné typy, ale ve skutečnosti máme jen jeden typ, který má různá jména - aliasy. 

TS tento kód vnímá takto: 

function sayHi(name: string): string {
	return ("Hi " + name)
}

Interface 

Interface je další způsob, jak popsat objektový typ. Jeho deklarace je podobná jako u aliasu, jen se definuje klíčovým slovem interface.

interface NameOfInterface {
	x: number;
	y: string;
}

Tento způsob funguje podobně, jako type aliasy. TS totiž neklade důraz na pojmenování typů, ale na strukturu a na to, co obsahují. Pokud tedy předáme funkci, která očekává objekt s určitou strukturou, je jedno, jak se objekt bude jmenovat, důležité ale je, aby splňoval strukturu. Proto se TS také nazývá strukturně typovaný typový systém.

Jaký je tedy rozdíl mezi type aliasy a interface?

Type aliasy a interfaces jsou velmi podobné a v mnoha případech záleží, co si vyberete. Hlavní rozdíl je ale v tom, že interface můžete vždy znovu otevřít a přidat nové vlastnosti nebo ho rozšířit.

Pokud chceme rozšířit interface, uděláme to následovně: 

interface Employee {
	id: number;
	name: string;
}

interface Developer extends Employee {
	seniority: string
}

Type alias lze taky rozšířit, a to pomocí průniku (intersection). Rozšíření by tedy vypadalo následovně: 

type Employee = {
	id: number;
	name: string;
}

type Developer = Employee & {
	seniority: string
}

Zde rozšiřujeme interface Developer, pomocí klíčového slova extends, rozhraním Employee o vlastnost seniority. Pokud bychom použili interface Developer, může využít všechny vlastnosti, které poskytují obě rozhraní.

Přidání nové vlastnosti do existujícího rozhraní lze udělat jednoduše. Definujeme si interface Room a následně přidáme novou vlastnost description. Tady máme znázorněno to, že interface můžeme znovu otevřít a přidat nové vlastnosti.

interface Room {
	id: number;
	title: string;
}

interface Room {
	description: string;
}

Pro type aliasy toto použít nemůžeme, jelikož bychom dostali error kvůli duplikování identifikátorů.

Type Assertions

Type assertions v TypeScriptu jsou užitečné pro situace, kdy máte více informací o typu hodnoty, než TypeScript dokáže vyhodnotit z kódu. Jde v podstatě o způsob, jakým můžete "říci" TypeScriptu, že konkrétní hodnota má určitý typ, i když to TypeScript ze samotného kódu nemůže rozpoznat.

Typický příklad je s metodou document.getElementById. Tato metoda vrací HTMLElement a v tomto konkrétním příkladě to bude HTMLCanvasElement s poskytnutým ID. Type assertion nám tedy umožní přesněji specifikovat tento konkrétnější typ. Máme možnost dvou zápisů:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas"); // kromě souborů .tsx

 TS dovoluje type assertions, které převádějí na specifičtější nebo obecnější verzi typu. Pokud bychom chtěli například převést číslo na string, dostaneme chybu: 

const a = 1 as string 

Type assertions jsou při kompilaci odstraněny TS kompilátorem a neovlivňují tak chování kódu za běhu aplikace. Tato informace je pouze pro kompilátor, aby pochopil kontext ohledně typů. Nemá také žádnou kontrolu chyb za běhu aplikace, takže potencionální chyba nevyvolá výjimku nebo chybu za běhu programu. 

Pokud jsou pravidla TS příliš omezující, můžeme využít tzv. dvojité assertions, kdy nejprve převedeme na typ any/unknown a pak až na požadovaný typ:

declare const x: any
const b = x as any as Array<string>

Literální typy

Kromě obecných typů string a number, můžeme v typových pozicích využít konkrétní čísla a řetězce. Jedním ze způsobů uvažování, je přístup JS k deklaraci proměnných. Var a let umožňují měnit to, co je uvnitř proměnné uloženo, tedy změnit hodnotu proměnné, což oproti tomu const nedovoluje. Tento přístup se odráží i TS, tedy ve způsobu, jakým přistupuje k tvorbě těchto typů. Pokud si deklarujeme proměnnou pomocí var nebo let, můžeme změnit hodnotu proměnné u tudíž je obecný předpis typu string. U konstanty to však zůstává deklarovaný string Hello world.

var changeMe = "Hello TypeScript"; //var changeMe: string
changeMe = "Hello world";

changeMe;

const constant = "Hello world"; //const constant = "Hello world"
constant

Stejně tak, když si deklarujeme doslovný typ. Tento typ však není příliš užitečný, kromě některých případů.

let x: hello = "hello";

// toto je v poradku
x = hello;

//zde nastane chyba v typu
x = hi

Pokud ale bychom chtěli vytvořit funkci, která přijímá pouze určitou množinu známých hodnot, stává se tento typ užitečný. Například máme funkci, která přijímá id a údaj, zda má být daný prvek posunutý na stranu pravou nebo levou.. 

function moveToSide(id: number, move: "right" | "left"){
	... code
}

Zde kombinujeme union typy a literální typy k dosažení co nejpřesnější deklarace funkce. Omezíme tím tedy množinu pohybu pouze na levou či pravou stranu a literální typy jsou teď velmi užitečným konceptem. Při snaze zadat jinou hodnotu, než right a left, dostaneme od TS chybu.

Stejným způsobem fungují i numerické literální typy. Návratová hodnota funkce compare musí být 0, -1 nebo 1. Pokud se pokusíme vrátit ve funkci něco jiného, dostaneme chybu.

function compare (a: number, b: number): -1 | 0 | 1 {
	return a === b ? 0 : a > b ? 1 : -1
}

Numerické hodnoty a řetězce můžeme také kombinovat. Můžeme si například definovat funkci, která přijímá konfiguraci výšky. Do funkce můžeme zadat buď specifickou výšku a nebo obecnou výšku, reprezentovanou hodnotou auto. Pokud se pokusíme do funkce zadat něco jiného, než číslo nebo řetězec auto, dostaneme chybu argumentů.

interface Height {
	height: number
}

function getHeight(h: Height | "auto"){
}

Null a undefined

Tyto dva datové typy nám nám značí nepřítomnost nebo neinicializovaný stav. Chování těchto hodnot se může lišit a to v závislosti na tom, zda máme zapnutý či vypnutý strictNullChecks, který můžeme definovat v konfiguraci TS. Tento přepínač nám pak ovlivňuje to, jak se bude s těmito hodnotami pracovat.  

Vypnutý strictNullChecks

Pokud je strictNullChecks v konfiguraci vypnutý, můžeme k hodnotám, které jsou null nebo undefined přistupovat zcela stejně, jako k ostatním hodnotám, aniž by to vyvolalo chyby. Stejně tak můžeme tyto hodnoty přiřadit jakékoli vlastni bez ohledu na její typ. Ale má to i své nevýhody. Přístup je sice více flexibilní a více se přibližuje k tradičnímu chování JS, ale nedostatek přísnosti v typování může vést k chybám a kód se tak stává náchylnější, jelikož nedochází ke kontrole null a undefined a ty jsou pak často zdrojem běhových chyb.

Zapnutý strictNullChecks

V tomto případě musíme testovat, zda hodnota náhodou není null nebo undefined. Stejně jako v případě optional parametrů, můžeme využít zúžení pro zjištění, zda je hodnota null nebo undefined:

function hello(message: string|null) {
	if	(message === null){
		console.log("There is no message")
	} else {
		console.log(message)
	}
}

Zde jednoduše kontrolujeme, zda je hodnota null. Stejně tak můžeme zkontrolovat, jestli je hodnota undefined.

TypeScript nám pro tento případ nabízí také operátor non-null assertion, který umožní zkontrolovat, zda daný typ není null nebo undefined. Zápis je jednoduchý, stačí za konkrétní hodnotu přidat !. Ani zde, stejně jako u ostatních typových tvrzení, se nemění chování kódu za běhu. Takže je důležité tento operátor používat v případě, že jsme si jisti, že hodnota nemůže být null nebo undefined.

function reverseNumbers(num?: Array<number> | null) {
  console.log(num!.reverse())
}

Enumy

Enumy jsou funkcí, kterou do JS přidává TS. Tato funkce umožňuje popsat hodnotu, která může být jednou z množiny definovaných hodnot. V jednoduchosti, definujeme si množinu, která obsahuje klíčové hodnoty a tyto hodnoty pak můžeme dále využít k porovnání, přiřazení a k dalším operacím.  Je zde rozdíl, že většina rozšíření TS se zabývá rozšířením na úrovni typů, ale enumy jsou rozšířením samotného jazyka JS a běhového prostředí.

Enumy jsou užitečné, když chceme vytvořit jednotnou sadu konstant, které mají přesný název a využívají se na mnoha místech. Když vznikne potřeba upravení nějaké hodnoty, stačí ji změnit na jednom místě a změny se projeví ve všech případech využití.

Numerické enumy

TS má jak řetězcové, tak číselné typy, což jsou ty základní. Výčet definujeme klíčovým slovem enum. Další věcí jsou hodnoty a jejich inicializace. Pokud neinicializujeme hodnoty, tak budou automaticky doplněny:

enum Rows {
	W,
	S,
	A,
	D
}

V tomto případě se začne od 0, takže Rows.W bude mít hodnotu 0, S 1 a tak dále. Pokud bychom ale chtěli číslovat například od 2, tak inicializujeme hodnotu W a další hodnoty budou 3, 4 a dál. Tento způsob můžeme aplikovat, pokud nám nezáleží na hodnotách a chceme jen odlišit hodnoty ve stejném výčtu

enum Rows {
	W = 2,
	S,
	A,
	D
}

K hodnotám pak přistupujeme jako k vlastnosti mimo samotný enum a deklarujeme typ hodnoty jako samotný enum. 

enum isError {
  Yes=1,
  No=0
}

function getNumber(value: number|string, message: isError): void {
  if (typeof value === 'number'){
    console.log(num, Boolean(message))
  }
  else {
    console.log(Boolean(message))
  }
}

Zde je jednoduchá funkce, která má dva parametry, value a message. Value nevíme, zda je number nebo string a druhý parametr je typu isError. Zkontrolujeme, zda value je typu number, pokud ano, vypíšeme value a error bude false, pokud bude string, vypíšeme chybu jako true. Tato funkce není zcela příklad z reálného využití, ale pro demonstraci použití je dostačující. Reálný případ bude uvedený v mini-projektu

Pokud máme enum, ve kterém chceme přiřadit hodnotu na základě nějaké funkce nebo jiného výpočtu, musíme si dát pozor na pořadí. Hodnoty bez inicializace musejí být na prvním místě, nebo přijít až za numerické hodnoty, které jsou inicializované numerickými konstantami nebo jinými konstantními členy enumů.

const getValueOf = () => 12;

//nepovolený zápis
enum Test {
  x = getValueOf(),
  y,
  z = 4
}

 

Řetězcové enumy

Tyto enumy fungují téměř stejně jako numerické, jen s tím rozdílem, že zde je třeba vždy uvést literální hodnotu nebo jinou hodnotu string enumu. 

enum Rows {
	W = "up",
	S = "down",
	A = "left",
	D = "right"
}

Tyto enumy sice nemají automatické zvyšovaní hodnot, ale mají velkou výhodu při debuggu, jelikož z ostatních enumů při debuggu většinou nedostaneme smysluplný význam o hodnotě. Řetězcové enumy tedy mají tu výhodu, že umožňují při běhu kódu udávat smysluplnou a čitelnou hodnotu nezávisle na názvu samotného člena enumu.

Pokud byste se chtěli více ponořit do enumů a dozvědět se více o tom, jak se chovají za běhu, vytvoření union typů a mnoho dalšího, najdete to zde. Veškeré funkce, které jsou v tomto článku, jsou i v připraveném mini-projektu. V tomto projektu naleznete implementace všech funkcí, o kterých jsme si povídali v tomto a předchozím článku.

Toto bylo základní přiblížení k TypeScriptu, jeho primárním typům a způsobům jejich jednoduchého použití. TypeScript je jazyk s bohatými možnostmi, a to, co jsme probrali v těchto dvou článcích, je jen malá část základů. Existuje mnohem více zajímavých konceptů na které se podíváme někdy příště.

První část

Pokud byste měli otázky, tak se určitě zastavte na náš Discord, kde můžeme vše prodiskutovat.

Zdroje

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html

https://www.typescriptlang.org/docs/handbook/enums.html