5 Complex TypeScript Problems Solved Using One Simple Example

Written by antonkrylov322 | Published 2022/07/23
Tech Story Tags: typescript | typescript-tutorial | javascript | javascript-development | javascript-frameworks | javascript-tutorial | javascript-fundamentals | software-development

TLDREvery day typescript projects become more and more complex, and people start to get confused in simple concepts. In the article, I will try to tell with a simple example how TS can be applied, and how to solve seemingly complex problems step by stepvia the TL;DR App

Why TS is important

So let's say for the first time you thought about writing something more complicated than a regular generic with one parameter. However, most often you rest against the fact that to use TS at an advanced level, you need to at least understand a little bit.

I thought about what to write this article when I watched the video:

https://www.youtube.com/watch?v=hBk4nV7q6-w

I was really in awe of what kind of things you can do on TS. of course, in hindsight, you realize that Turing-complete language is capable of anything. But to do so for everything ...

Fortunately, most of the complex shit on the TS remains a lot of those who like to solve complex puzzles so that you can fly here and solve 500 problems in nano sec. We are cool experienced men sitting and solving real problems in the real world. But I want to point out.

The larger your codebase, the more you need ADVANCED TS. I am not kidding. Otherwise, your codebase will bloat like another Kotlin, Spring Boot project led by 10 java monkeys. Without competent reuse of types, generics, and other clever things, your project will slide into a poop.

Simple TS problem

Okay, boy, we believe that you are right - now show us what to do to make our project that Mark Zuckerberg will buy it for 9999999999 dollars for a beautiful code.

The first thing to learn is, of course, Partial, Pick, Omit, Exclude, Extract, how &/|, branding-types work, how to work with recursive types, etc. This is a must-have.

And all these things you need to be able to use perfectly in combination. I won’t throw in terms, Google can do it without me, I’ll tell you with a simple example of widget typing in my project.

type GenericWidget<T, V> = { type: T, name: string, value: V }

type Sizes = '256' | '1024';
type TextWidget = GenericWidget<'text', string>;
type ImageWidget = GenericWidget<'image', { [Size in Sizes as `${Size}x${Size}`]: string; }>

type Widget = TextWidget | ImageWidget

const widgets: Widget[] = [{ type: 'text', name: 'text', value: 'text' }, { type: 'image', name: 'image', value: { '1024x1024': 'https ://s1.1zoom.ru/big3/61/Cats_Glance_Whiskers_Snout_516618_5338x3559.jpg' } }]

type GetWidgetByType<T extends Widget['type']> = Extract<Widget, { type: T }>

export const getWidgetByType = <T extends Widget['type'], WT = GetWidgetByType<T>>(
    type: T
) => (widgets: Widget[]): WT | undefined =>
    widgets.find((widget) => widget.type === type) as WT | undefined;

Branded type

// branded type
type GenericWidget<T, V> = { type: T, name: string, value: V }

Let's take it apart piece by piece - this is branded type. In short, we have some property by which we can say oh - this type is actually like this. Approximately the same as if we were doing a key-value map, only this option also works with an array.

Template literal

Further, everything is quite simple. We declare widgets. The most interesting here is probably the 3rd line. Using string literals in combination with Size

type Sizes = '256' | '1024';
type TextWidget = GenericWidget<'text', string>;
// template literal
type ImageWidget = GenericWidget<'image', { [Size in Sizes as `${Size}x${Size}`]: string; }>

type Widget = TextWidget | ImageWidget

It's funny that for the first time I started writing without understanding

type ImageWidget = GenericWidget<'image', { `${Sizes}x${Sizes}`: string }>

Remember that literals need specific strings, not some abstract ones (although we can write it this way, and it will work just fine too)

type ImageWidget = GenericWidget<'image', { [Size in string as `${Size}x${Size}`]: string; }>

Then we taste Extract, and we declare Generic, which will get our widget by type. Since we previously declared it via BrandedType, we will get exactly one type.

const widgets: Widget[] = [{ type: 'text', name: 'text', value: 'text' }, { type: 'image', name: 'image', value: { '1024x1024': 'https ://s1.1zoom.ru/big3/61/Cats_Glance_Whiskers_Snout_516618_5338x3559.jpg' } }]

type GetWidgetByType<T extends Widget['type']> = Extract<Widget, { type: T }>

Next, we declare the getWidgetByType function, which takes a type, and then searches for widgets on which this type is used

const getWidgetByType = <T extends Widget['type'], WT = Extract<Widget, { type: T }>>(
    type: T
) => (widgets: Widget[]): WT | undefined =>
widgets.find((widget) => widget.type === type) as WT | undefined;

Usage example:

const getTextWidget = getWidgetByType('text');
const textWidget = getTextWidget(widgets)

An attentive reader may have noticed that BrandedType does not oblige us to make widget types unique. Accordingly, if we do like this:

type TextWidget = GenericWidget<'text', string>;
type NumberWidget = GenericWidget<'text', number>;
type Widget = TextWidget | Image Widget | NumberWidget
type Kek = GetWidgetByType<'text'>

Then we get the type Kek ~= TextWidget | NumberWidget. And if we allow we will declare widgets in different files, then we can fall into the trap of a wildcard with a combined type. What can we do in such a situation? Shtosh, with a little thought, we can get such a drug addiction:

type GenericWidget<T, V> = { type: T, name: string, value: V }

type Sizes = '256' | '1024';

type InnerWidget = {
    text:string
    number: number,
    image: { [Size in Sizes as `${Size}x${Size}`]?: string; }
}

type ObjectToUnion<O> = {[Key in key of O as number]: { type: Key; name:string; value: O[Key] }}[number]

type Widget = ObjectToUnion<InnerWidget>

type GetWidgetByType<T extends Widget['type']> = Extract<Widget, { type: T }>

type Kek = GetWidgetByType<'image'>

We do the same thing, only through the InnerWidget map. The most interesting thing happens here.

Object to list type converter

type ObjectToUnion<O> = {[Key in key of O as number]: { type: Key; name:string; value: O[Key] }}[number]

Approximately in this way, any object can be turned into a Union. Let's figure out what's going on here.

  1. We loop through all the keys in the object https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
  2. Since we want to combine all this later, we need to do it as a number so that TS believes that this is an array
  3. We declare our newly minted object denoting the keys as type and the values ​​as value
  4. Indexed by the freshly baked array https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

Now everything works like clockwork, but I'm too lazy to rewrite the floor of the project under the new high rules. Suddenly fall apart.

As they say, if the point is pressed, then it is urgent to write tests. But how, these are fucking ts types. Don't worry, smart people have already taken care of this. When I researched this topic, of course, I was crazy about WHAT solutions the guys are doing.

For example here, people just patched the TS for themselves and made a heavy lib. Don’t do so. Although maybe someone likes pain and suffering. After all, for this, we went to the Frontend - right?

One way or another, let's take something nicer and more understandable, and Schaub's code is smaller. While studying the topic, I found a super buzz.

One way or another, they will all be based on a simple generic Equal. For those who are interested, read the implementation, there are only 200 lines based on these generics

Conditional type

export type Not<T extends boolean> = T extends true ? false : true
export type Eq<Left extends boolean, Right extends boolean> = Left extends true ? Right : Not<Right>

What is going on here? Let's figure it out

  1. To properly understand conditional expressions in TS, you need to remember the type diagram. Where the arrow points, that type is inherited)

  2. Still kicking the doc

export type Not<T extends boolean> = T extends true ? false : true
// Just invert boolean T extends true in TS language T === true because this is the final literal
// Google translate -> Not(T) = T === true ? false : true
export type Eq<Left extends boolean, Right extends boolean> = Left extends true ? Right : Not<Right>
// if left === true, then for equality right must be equal to true -> Right
// else right should be false -> not(Right)
// You may ask why not Left extends Right ? true : false?
// I don't know either, but the behavior of these generics is different, so let's trust the author))

Type test

import {expectTypeOf} from 'expect-type'

type GenericWidget<T, V> = { type: T, name: string, value: V }

type Sizes = '256' | '1024';
type TextWidget = GenericWidget<'text', string>;
type NumberWidget = GenericWidget<'text', number>;
type ImageWidget = GenericWidget<'image', { [Size in Sizes as `${Size}x${Size}`]?: string; }>

type Widget = TextWidget | Image Widget | NumberWidget

type GetWidgetByType<T extends Widget['type']> = Extract<Widget, { type: T }>

expectTypeOf<GetWidgetByType<'text'>['value']>().toBeString()

As we see our test will fall. Accordingly, we can cover our generics with basic tests and believe that this Library provides many features and has a lot of interesting code in the implementation.

Be sure to read what is written there.

If you are a library developer, or you lack the functionality of regular TS, or you just like to have fun with TS, look towards this library.

Afterwords

You can talk all you want about how complicated this Typescript is, but one thing is for sure - you can do great things with it. If someone is interested in the topic of type tests, I will also write an article on this topic.


Written by antonkrylov322 | Frontend developer with taste of beer
Published by HackerNoon on 2022/07/23