Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 20, 2021 02:46 pm GMT

Advanced typescript for React developers - part 3

Image description

This is a third article in the series of Advanced typescript for React developers. In the previous chapters we together with ambitious developer Judi figured out how and why Typescript generics are useful for creating reusable React components, and understood such typescript concepts as type guards, keyof, typeof, is, as const and indexed types. We did it while implementing with Judi a competitor to Amazon: an online website that has different categories of goods and the ability to select them via a select component. Now its time to improve the system once again, and to learn in the process what is the purpose of exhaustiveness checking, how the narrowing of types works and when typescript enums could be useful.

You can see the code of the example were starting with in this codesandbox.

Exhaustiveness checking with never

Lets remember how we implemented our Tabs with categories. We have an array of strings, a switch case that for every tab returns a select component, and a select component for categories themselves.

const tabs = ["Books", "Movies", "Laptops"] as const;type Tabs = typeof tabs;type Tab = Tabs[number];const getSelect = (tab: Tab) => {  switch (tab) {    case "Books":      return (        <GenericSelect<Book> ... />      );    case "Movies":      return (        <GenericSelect<Movie> ... />      );    case "Laptops":      return (        <GenericSelect<Laptop> ... />      );  }};export const TabsComponent = () => {  const [tab, setTab] = useState<Tab>(tabs[0]);  const select = getSelect(tab);  return (    <>      Select category:      <GenericSelect<Tab>        onChange={(value) => setTab(value)}        values={tabs}        formatLabel={formatLabel}      />      {select}    </>  );};

Everything is perfectly typed, so if a typo happens anywhere it will be picked up by Typescript. But is it perfectly typed though? What will happen if I want to add a new category to the list: Phones? Seems easy enough: I just add it to the array and to the switch statement.

const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;const getSelect = (tab: Tab) => {  switch (tab) {    // ...    case "Phones":      return (        <GenericSelect<Phone> ... />      );  }};

And in a simple implementation like this, it wouldnt bring much trouble. But in real life more likely than not this code will be separated, abstracted away, and hidden behind layers of implementation. What will happen then if I just add Phones to the array, but forget about the switch case?

const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;const getSelect = (tab: Tab) => {  switch (tab) {    case "Books":      // ...    case "Movies":      // ...    case "Laptops":      // ...  }};

With this implementation - nothing good, unfortunately. Typescript will be totally fine with it, the bug might be missed during manual testing, it will go to production, and when customers select Phones in the menu, they wont see anything on the screen.

It doesnt have to be like this though. When we use operators like if or switch typescript performs what is known as narrowing, i.e. it reduces the available options for the union types with every statement. If, for example, we have a switch case with only Books, the Books type will be eliminated at the first case statement, but the rest of them will be available later on:

const tabs = ["Books", "Movies", "Laptops"] as const;// Just "Books" in the switch statementconst getSelect = (tab: Tab) => {  switch (tab) {    case "Books":      // tab's type is Books here, it will not be available in the next cases      return <GenericSelect<Book> ... />    default:      // at this point tab can be only "Movies" or "Laptops"      // Books have been eliminated at the previous step  }};

If we use all the possible values, typescript will represent the state that will never exist as never type.

const tabs = ["Books", "Movies", "Laptops"] as const;const getSelect = (tab: Tab) => {  switch (tab) {    case "Books":      // "Books" have been eliminated here    case "Movies":      // "Movies" have been eliminated here    case "Laptops":      // "Laptops" have been eliminated here    default:      // all the values have been eliminated in the previous steps      // this state can never happen      // tab will be `never` type here  }};

And watch the hands very carefully for this trick: in this impossible state you can explicitly state that tab should be never type. And if for some reason its not actually impossible (i.e. we added Phones to the array, but not the switch - typescript will fail!

// Added "Phones" here, but not in the switchconst tabs = ["Books", "Movies", "Laptops", "Phones"] as const;// Telling typescript explicitly that we want tab to be "never" type// When this function is called, it should be "never" and only "never"const confirmImpossibleState = (tab: never) => {  throw new Error(`Reacing an impossible state because of ${tab}`);};const getSelect = (tab: Tab) => {  switch (tab) {    case "Books":      // "Books" have been eliminated    case "Movies":      // "Movies" have been eliminated    case "Laptops":      // "Laptops" have been eliminated    default:      // This should be "impossible" state,      // but we forgot to add "Phones" as one of the cases      // and "tab" can still be the type "Phones" at this stage.      // Fortunately, in this function we assuming tab is always "never" type      // But since we forgot to eliminate Phones, typescript now will fail!      confirmImpossibleState(tab);  }};

Now the implementation is perfect! Any typos will be picked up by typescript, non-existing categories will be picked up, and missed categories will be picked up as well! This trick is called Exhaustiveness checking by the way.

Exhaustiveness checking without never

Interestingly enough, for the exhaustiveness trick to work, you dont actually need never type and the impossible state. All you need is just to understand this process of narrowing and elimination, and how to lock the desired type at the last step.

Remember, we had our formatLabel function that we pass to the select component, that returns the desired string for the select options based on the value type?

export type DataTypes = Book | Movie | Laptop | string;export const formatLabel = (value: DataTypes) => {  if (isBook(value)) return `${value.title}: ${value.author}`;  if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;  if (isLaptop(value)) return value.model;  return value;};

Another perfect candidate for exactly the same bug - what will happen when we add Phone as one of the data types, but forget the actual check? With the current implementation - nothing good again, the Phone select options will be broken. But, if we apply the exhaustiveness knowledge to the function, we can do this:

export type DataTypes = Book | Movie | Laptop | Phone | string; // When this function is called the value should be only string const valueShouldBeString = (value: string) => value; const formatLabel = (value: DataTypes) => {  // we're eliminating Book type from the union here  if (isBook(value)) return `${value.title}: ${value.author}`;  // here value can only be Movie, Laptop, Phone or string  // we're eliminating Movie type from the union here  if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;  // here value can only be Laptop, Phone or string  // we're eliminating Laptop type from the union here  if (isLaptop(value)) return value.model;  // here value can only be Phone or string  // But we actually want it to be only string  // And make typescript fail if it is not  // So we just call this function, that explicitly assigns "string" to value  return valueShouldBeString(value);  // Now, if at this step not all possibilities are eliminated  // and value can be something else other than string (like Phone in our case)  // typescript will pick it up and fail!};

We have eliminated all the possible union types except string, and locked string in the final step. Pretty neat, huh?

See fully working example in this codesandbox.

Improving code readability with Enums

Now its the time for the final polish of this beautiful piece of typescript art that is our categories implementation. I dont know about you, but this part worries me a bit:

const tabs = ["Books", "Movies", "Laptops"] as const;type Tabs = typeof tabs;type Tab = Tabs[number];

There is nothing wrong with it per se, it just slightly breaks my brain every time Im looking at the constructs like that. It always takes one-two additional seconds to understand what exactly is going on here. Fortunately, there is a way to improve it for those who suffer from the same issue. Did you know that Typescript supports enums? They allow defining a set of named constants. And the best part of it - those are strongly typed from the get-go, and you can literally use the same enum as type and as value at the same time.

Basically this:

const tabs = ["Books", "Movies", "Laptops"] as const;type Tabs = typeof tabs;type Tab = Tabs[number];

Could be replaced with this, which is arguably much easier and more intuitive to read:

enum Tabs {  'MOVIES' = 'Movies',  'BOOKS' = 'Books',  'LAPTOPS' = 'Laptops',}

And then, when you need to access a specific value, youd use dot notation, just like an object:

const movieTab = Tabs.MOVIES; // movieTab will be `Movies`const bookTab = Tabs.BOOKS; // bookTab will be `Books`

And just use Tabs when you want to reference the enum as a type!

If we look at our tabs code, we can just replace all the Tab types with enum Tabs and all the tabs strings with enums values:

Image description

And, in the actual implementation of the Tabs component the same: replace the type, replace values, and pass to select component enums values in the form of an array:

Image description

See the full code example in this codesandbox.

Perfection!

That is all for today, hope you enjoyed the reading and now feel a little bit more confident with typescripts narrowing, exhaustiveness checking and enums. See ya next time

...

Originally published at https://www.developerway.com. Check out the website for more articles like this. Subscribe to the newsletter to get notified as soon as the next article comes out.


Original Link: https://dev.to/adevnadia/advanced-typescript-for-react-developers-part-3-p4j

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To