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.
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
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.
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.
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.
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
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>
To properly understand conditional expressions in TS, you need to remember the type diagram. Where the arrow points, that type is inherited)
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))
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.
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.