středa 18. července 2018

React a hrátky s TypeScriptem


V minulosti jsem se již několikrát zmiňoval, že používat JavaScript bez statických typů, je stejné jako jezdit na kole poslepu. Nemusí se Vám nic stát, ale také si můžete hezky ublížit. Jednou z variant, jak částečně předcházet problémům, je použití staticky typovaného jazyka. Už během psaní kódu je více viditelné, že "něco není v pořádku". TypeScript (dále jen TS) je jazyk, který nám k tomuto účelu může dobře posloužit.

Cílem dnešního článku jsou příklady, na kterých se pokusím demonstrovat vlastnosti jazyka, se kterými se lze setkat. Výčet určitě není kompletní, protože TS je velice sofistikovaný jazyk, který se nedá popsat ani jednou knihou.

Generické typy


Jak jednou řekl můj bývalý kolega: "Generické typy? To je ten zápis s kachníma zobákama, ne?" :)

Generické typy jsou součástí snad každého staticky typovaného jazyka. Generiky naleznete jak v Jave, C#, tak právě i v TS. Pokud v TS píšete, tak neexistuje téměř možnost, že byste se s generickými typy nesetkali.

Pojďme se podívat na jednoduchý příklad:
interface Props {
    firstName: string;
    lastName: string;
}
const User: React.SFC<Props> = ({firstName, lastName}) => (
    <div>
        {firstName} {lastName}
    </div>
);
const App = () => <User firstName={'Ales'} lastName={'Dostal'} />;

Zde je vidět, že je použita generika v React.SFC, což je typ, který definuje, že se jedna o React Stateless komponentu. Součástí tohoto typu je možnost uvést generický typ, tedy předem neznámý typ, který ovšem po deklaraci, bude kontrolován.

Zjednodušeně se dá říci, že pokud bychom v komponentě User definovali neznámý atribut, bude nám TS hlásit chybu:
Chybný atribut foo 
Jejich využití má největší přidanou hodnotu ve chvíli, kdy píšete kód, který má využití na více místech. Generické typy nabízí právě tu možnost, aby daný kód byl co nejvíce variabilní.

Od verze TS 2.9 je možné generické typy používat i přímo v JSX. Díky tomu je například snadnější typovat render props v Reactu.

Enum a string literal types


Často se dostáváme do situací, kdy je třeba definovat přesný výčet hodnot, které lze použít. 

Pojďme si rozšířit naší komponentu o novou vlastnost:
type UserType = 'admin' | 'guest';

interface Props {
    firstName: string;
    lastName: string;
    type: UserType;
}

const User: React.SFC<Props> = ({firstName, lastName}) => (
    <div>
        {firstName} {lastName}
    </div>
);
const App = () => <User firstName={'Ales'} lastName={'Dostal'} type={'admin'} />;

Nyní jsme přidali typ uživatele. Tím typem může být administrátor či host.

Pokud bychom do atributu type vložili jiný typ, tak TS zahlásí chybu:
Chybný typ uživatele
Kromě možnosti definovat výčet pomocí string literal types, tak je možné onen typ definovat i pomocí enum.

Příklad:
enum UserType {
    ADMIN = 'admin',
    GUEST = 'guest',
}
const App = () => <User firstName={'Ales'} lastName={'Dostal'} type={UserType.ADMIN} />;


Type queries a typeof


JavaScript disponuje klíčovým slovem typeof, který umí číst typy z jakéhokoliv objektu. Velice efektivně toho dá využít pro pro vytvoření typu z objektu.

Ano, zní to možná divně, ale pojďme se podívat na ukázku:
const initialState = {count: 0};

class User extends React.Component<Props, typeof initialState> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };
    
    render() {
        const {firstName, lastName} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {firstName} {lastName} | Count: {count}
            </div>
        );
    }
}

Komponentu User jsme přepsali do třídy a přidali State. Součástí toho je výchozí state, který komponenta očekává. V našem případě začíname na count = 0. Proto jsme využili toho, že podle nadefinovaného výchozí stavu, jsme vytvořili i daný typ State. Stejným způsobem bychom mohli použít typeof i v případě defaultních hodnot pro props.

Readonly a readonly


Nejen v Reactu bychom se měli snažit o to, abychom psali více funkcionálně. Současně s tím je důležité se zaměřit na immutable stav. Tedy najít způsob, jak zabezpečit, aby daná hodnota nešla přepisovat přímo, ale vždy se vytvářela pouze nová kopie.  K tomuto účelu můžeme využít readonly, který nám zajišťuje, že daný atribut v objektu bude immutable (alespoň z pohledu TS).

Pojďme na ukázku:
interface Props {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {firstName, lastName} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {firstName} {lastName} | Count: {count}
            </div>
        );
    }
}

V ukázce jsou použity dvě varianty. První variantou je definování u props, kde každý atribut je označen pomocí klíčového slova readonly. Druhou variantou je State, kde je použit typ Readonly. Pokud bychom nyní chtěli napřímo mutovat atributy Props či State, tak TS nám bude hlásit chybu, že daný atribut je readonly.

Pick


Další skvělou vlastností je typ Pick. Tento typ slouží k tomu, abychom si z předem definovaného typu, vytvořili nový typ, který bude obsahovat pouze ty atributy, které dopředu určíme.

Pojďmě opět na ukázku:
type UserType = 'admin' | 'guest';

interface UserDataQuery {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

interface Query {
    data: UserDataQuery;
    roles: string[];
}

interface Props extends Pick<Query, 'data'> {}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {data} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {data.firstName} {data.lastName} | Count: {count}
            </div>
        );
    }
}

const App = () => <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />;

Často se setkáváme s tím, že máme například z GraphQL vygenerovaný typový model, ale ten, na úrovni Query obsahuje všechny možné atributy, které lze získat. Zde se výborně hodí onen typ Pick, který nám vytvoří typ, který vychází z typu Query. Obsahovat bude pouze ty atributy, které si sami určíme. V našem případě tedy atribut data.

Omit


Pokud pomocí typu Pick můžeme definovat atributy, které nový objekt má obsahovat, jeho protějškem je typ Omit. Ten naopak umí definovat, které atributy nový typ obsahovat nebude.

Pojďme na ukázku (v ukázce je Omit použi z knihovny recompose):
interface UserQuestProps {
    data: Omit<UserDataQuery, 'type'>;
}

const UserGuest: React.SFC<UserQuestProps> = (props) => {
    return <User data={{...props.data, type: 'guest'}} />;
};

const App = () => (
    <div>
        <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />
        <UserGuest data={{firstName: 'Ales', lastName: 'Dostal'}} />
    </div>
);

V tomto případě jsme vytvořili novou komponentu UserGuest, která ovšem umožňuje, že i když vychází ze stejného datového typu UserDataQuery, tak odstraňuje atribut type, který je přímo nastaven na hodnotu guest.

V tomto případě je na zvážení, zda by ona komponenta UserGuest, neměla být napsána spíše jako HOC (Higher-Order Component). Ale o tom až jindy :)

Závěr


Vybral jsem pár zajímavých vlastností TS jazyka, se kterými se lze často setkat a zároveň i věci, které nejsou až tak známé (viz Pick či Omit). Příště se zkusíme podívat na některé další vlasnosti.

Na závěr ještě celá ukázka našeho příkladu:
type UserType = 'admin' | 'guest';

interface UserDataQuery {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

interface Query {
    data: UserDataQuery;
    roles: string[];
}

interface Props extends Pick<Query, 'data'> {}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {data} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {data.firstName} {data.lastName} | Count: {count}
            </div>
        );
    }
}

interface UserQuestProps {
    data: Omit<UserDataQuery, 'type'>;
}

const UserGuest: React.SFC<UserQuestProps> = (props) => {
    return <User data={{...props.data, type: 'guest'}} />;
};

const App = () => (
    <div>
        <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />
        <UserGuest data={{firstName: 'Ales', lastName: 'Dostal'}} />
    </div>
);

Žádné komentáře:

Okomentovat

React a hrátky s TypeScriptem

V minulosti jsem se již několikrát zmiňoval, že používat JavaScript bez statických typů, je stejné jako jezdit na kole poslepu. Nemusí se...