Advanced typescript for React developers

Introduction

type Base = {
id: string;
title: string;
};

type GenericSelectProps<TValue> = {
values: TValue[];
onChange: (value: TValue) => void;
};

export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);

if (val) onChange(val);
};

return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
<GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />
<GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />
type Laptop = {
id: string;
model: string;
releaseDate: string;
}

// This will fail, since there is no "title" in the Laptop type
<GenericSelect<Laptop> onChange={(value) => console.log(value.model)} values={laptops} />

Rendering not only titles in options

type Laptop = {
id: string;
model: string;
releaseDate: string;
}

type LaptopKeys = keyof Laptop;
<GenericSelect<Laptop> titleKey="model" {...} />
// inside GenericSelect "titleKey" will be typed to "id" | "model" | "releaseDate"

<GenericSelect<Book> titleKey="author" {...} />
// inside GenericSelect "titleKey" will be typed to "id" | "title" | "author"
type Base = {
id: string;
title?: string;
}

export const GenericSelect = <TValue extends Base>(props: GenericSelectProps<TValue>) => {

The list of categories — refactor select

  1. Convert Base type into something that understands strings as well as objects
  2. Get rid of reliance on value.id as the unique identificator of the value in the list of options
  3. Convert value[titleKey] into something that understands strings as well
type Base = { id: string } | string;

// Now "TValue" can be either a string, or an object that has an "id" in it
export const GenericSelect = <TValue extends Base>(props: GenericSelectProps<TValue>) => {
type Base = { id: string } | string;

const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (typeof value === 'string') {
// here "value" will be the type of "string"
return value;
}

// here "value" will be the type of "NOT string", in our case { id: string }
return value.id;
};
type GenericSelectProps<TValue> = {
formatLabel: (value: TValue) => string;
...
};

export const GenericSelect = <TValue extends Base>(props: GenericSelectProps<TValue>) => {
...
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={getStringFromValue(value)} value={getStringFromValue(value)}>
{formatLabel(value)}
</option>
))}
</select>
);
}

// Show movie title and release date in select label
<GenericSelect<Movie> ... formatLabel={(value) => `${value.title} (${value.releaseDate})`} />

// Show laptop model and release date in select label
<GenericSelect<Laptop> ... formatLabel={(value) => `${value.model, released in ${value.releaseDate}`} />
type Base = { id: string } | string;

type GenericSelectProps<TValue> = {
formatLabel: (value: TValue) => string;
onChange: (value: TValue) => void;
values: TValue[];
};

const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (typeof value === 'string') return value;

return value.id;
};

export const GenericSelect = <TValue extends Base>(props: GenericSelectProps<TValue>) => {
const { values, onChange, formatLabel } = props;

const onSelectChange = (e) => {
const val = values.find((value) => getStringFromValue(value) === e.target.value);

if (val) onChange(val);
};

return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={getStringFromValue(value)} value={getStringFromValue(value)}>
{formatLabel(value)}
</option>
))}
</select>
);
};

The list of categories — implementation

const tabs = ['Books', 'Movies', 'Laptops'];

const getSelect = (tab: string) => {
switch (tab) {
case 'Books':
return <GenericSelect<Book> onChange={(value) => console.info(value)} values={books} />;
case 'Movies':
return <GenericSelect<Movie> onChange={(value) => console.info(value)} values={movies} />;
case 'Laptops':
return <GenericSelect<Laptop> onChange={(value) => console.info(value)} values={laptops} />;
}
}

const Tabs = () => {
const [tab, setTab] = useState<string>(tabs[0]);

const select = getSelect(tab);


return (
<>
<GenericSelect<string> onChange={(value) => setTab(value)} values={tabs} />
{select}
</>
);
};
// an array of values type "string"
const tabs = ['Books', 'Movies', 'Laptops'];

tabs.forEach(tab => {
// typescript is fine with that, although there is no "Cats" value in the tabs
if (tab === 'Cats') console.log(tab)
})

// an array of values 'Books', 'Movies' or 'Laptops', and nothing else
const tabs = ['Books', 'Movies', 'Laptops'] as const;

tabs.forEach(tab => {
// typescript will fail here since there are no Cats in tabs
if (tab === 'Cats') console.log(tab)
})
const tabs = ['Books', 'Movies', 'Laptops'];
type Tabs = typeof tabs; // Tabs will be string[];

const tabs = ['Books', 'Movies', 'Laptops'] as const;
type Tabs = typeof tabs; // Tabs will be ['Books' | 'Movies' | 'Laptops'];
type Tab = Tabs[number]; // Tab will be 'Books' | 'Movies' | 'Laptops'
type LaptopId = Laptop['id']; // LaptopId will be string

Bonus: type guards and “is” operator

type Base = { id: string } | string;

const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (typeof value === 'string') {
// here "value" will be the type of "string"
return value;
}

// here "value" will be the type of "NOT string", in our case { id: string }
return value.id;
};
type Base = { id: string } | string;

const isStringValue = <TValue>(value: TValue) => return typeof value === 'string';

const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (isStringValue(value)) {
// do something with the string
}

// do something with the object
};
const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (isStringValue(value)) { // it's just a random function that returns boolean
// type here will be unrestricted, either string or object
}

// type here will be unrestricted, either string or object
// can't return "value.id" anymore, typescript will fail
};
type T = { id: string };
// can't extend Base here, typescript doesn't handle generics here well
export const isStringValue = <TValue extends T>(value: TValue | string): value is string => {
return typeof value === 'string';
};
type T = { id: string };
type Base = T | string;

export const isStringValue = <TValue extends T>(value: TValue | string): value is string => {
return typeof value === 'string';
};

const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (isStringValue(value)) {
// do something with the string
}

// do something with the object
};
export type DataTypes = Book | Movie | Laptop | string;

export const isBook = (value: DataTypes): value is Book => {
return typeof value !== 'string' && 'id' in value && 'author' in value;
};
export const isMovie = (value: DataTypes): value is Movie => {
return typeof value !== 'string' && 'id' in value && 'releaseDate' in value && 'title' in value;
};
export const isLaptop = (value: DataTypes): value is Laptop => {
return typeof value !== 'string' && 'id' in value && 'model' in value;
};
const formatLabel = (value: DataTypes) => {
// value will be always Book here since isBook has predicate attached
if (isBook(value)) return value.author;

// value will be always Movie here since isMovie has predicate attached
if (isMovie(value)) return value.releaseDate;

// value will be always Laptop here since isLaptop has predicate attached
if (isLaptop(value)) return value.model;

return value;
};

// somewhere in the render
<GenericSelect<Book> ... formatLabel={formatLabel} />
<GenericSelect<Movie> ... formatLabel={formatLabel} />
<GenericSelect<Laptop> ... formatLabel={formatLabel} />

Time for goodbye

  • “keyof” — use it to generate types from keys of another type
  • “as const” — use it to signal to typescript to treat an array or an object as a constant. Use it with combination with “type of” to generate actual type from it.
  • “typeof” — same as normal javascript "typeof", but operates on types rather than values
  • Type['attr'] or Type[number] - those are indexed types, use them to access subtypes in an Object or an Array respectively
  • argName is Type - type predicate, use it to turn a function into a safeguard

--

--

--

Frontend architect, coder. Love solving problems, fixing things and writing in-depth tech articles: https://www.developerway.com

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

React Interview Questions-1

Uboric.com- Clone

Daily Progression: Define A Case!

“My mind goes working”: Calculating Knight Moves in JavaScript.

React 3030 — Episode 2 (Project Folder Structure)

ReactJS Prerequisites

Vue.js 3 Composition API: the SetUp Function

Coding Puzzles[EP2]: What Anime to Watch

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nadia Makarevich

Nadia Makarevich

Frontend architect, coder. Love solving problems, fixing things and writing in-depth tech articles: https://www.developerway.com

More from Medium

How to Write Performant React Apps with Context

Why you should use Error Boundaries in React

Clean up your React component types 🧼

Two-way binding will make your React code better.