Skip to content

Commit 3744a9f

Browse files
author
Mat Dudek
committed
📝 docs: add Methods type documentation
1 parent bfed70d commit 3744a9f

File tree

1 file changed

+133
-19
lines changed

1 file changed

+133
-19
lines changed

docs/complex-types/methods.md

Lines changed: 133 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,154 @@
22

33
In this section we will explore 🚀 all nooks and crannies behind methods types.
44

5+
## Methods
56

7+
### Why?
8+
So basically type `Methods` is defined to declare how methods within Entity are built.\
9+
It will be an object with keys and values
610

7-
Diving into `methods.ts` we have these three imports
8-
9-
```ts
10-
import { EntitySchema } from "./data" // Record<string, any>
11-
import { SyncKey } from "./sync" // Opaque<string, "sync-key">
12-
import { Except } from "type-fest" // Create a type from an object type without certain keys.
13-
```
14-
15-
## Methods
1611

12+
### How?
1713
```ts
1814
export type Methods<TSchema extends EntitySchema> = Record<
1915
string, // keys
2016
(this: TSchema, ...args: any[]) => any // values
2117
>
2218
```
2319
24-
`Methods<TSchema>`: This is a generic type called `Methods`. It takes a type parameter `TSchema`, which is expected to extend the `EntitySchema`.<br/>
25-
This means that `Methods` is a reusable type where we can use it for different EntityType. (dunno if I should explain this @TODO)
20+
::: info EntitySchema
21+
Record<string, any>
22+
:::
23+
24+
It's a `Record` that has keys as strings & methods as values.\
25+
The values part seems to be tricky. It indicates a methods that takes
26+
- `this` of type `TSchema` as it's first parameter.
27+
- `..args: any[]` as rest. (It means any number of arguments of any type)
28+
29+
::: details Why we indicate `this` of type `TSchema`?
30+
All methods have `this` inside and we must declare it's type. Without it we would get an `unknown` type instead.
31+
We assume that all methods in `Entity` are going to have access to data layer. By defining `this: TSchema` we are giving correct type to `this`. This means that `this` inside entity function will be type of data layer and user will be able to use it without errors.
32+
:::
33+
34+
## Prototype Methods
2635
36+
### Why?
37+
Prototype Methods are default methods inside every entity.\
38+
It doesn't matter what Entity we are going to build we will always have this base methods inside.
39+
40+
### How?
41+
42+
It's an `object` with four keys.
2743
2844
```ts
29-
Record<string, ...>
45+
export type PrototypeMethods<TSchema extends EntitySchema> = {
46+
toObject(): TSchema
47+
toJson(): string
48+
isSynced(id: SyncKey): boolean
49+
setSynced(id: SyncKey, promise: Promise<unknown>): void
50+
}
3051
```
31-
This denotes as an __*object*__ type where keys are `strings`. Pretty logic, methods must be somehow named.
52+
::: info SyncKey
53+
Opaque<string, "sync-key"> (Branded type)
54+
:::
55+
56+
The curious part here is `setSynced`.\
57+
`setSynced` method takes two parameters (id of type `SyncKey` and promise of type `Promise<unknown>`) and doesn't return any value (`void`).\
58+
This type signature is commonly used in TypeScript to declare function that perform some action or side effects without returning any value.\
59+
This method does not handle the promise resolution itself but expects the consumer to chain a `.then` callback for side effects and update sync status.
60+
61+
## ResolvedMethods
62+
63+
### Why?
64+
This type is a mechanism for ensuring that the user-defined methods for an `EntitySchema` are appropriately augmented and validated based on a set of `PrototypeMethods`.
65+
66+
::: info
67+
When factory function infers `TMethods` without union with undefined it set value of the `TMethods` to `Methods<TSchema>` - which makes it impossible to detect on the type level if the user defined any methods but when we set `TMethods` to `Methods<TSchema> | undefined` it will be possible to detect it.
68+
:::
69+
70+
### How?
3271
3372
```ts
34-
(this: TSchema, ...args: any[]) => any
73+
export type ResolvedMethods<
74+
TSchema extends EntitySchema,
75+
TMethods extends Methods<TSchema> | undefined,
76+
> = (
77+
TMethods extends Methods<TSchema>
78+
? TMethods extends undefined
79+
? "false"
80+
: "true"
81+
: 0
82+
) extends "true"
83+
? TMethods extends Methods<TSchema>
84+
? {
85+
[Key in Exclude<
86+
keyof PrototypeMethods<TSchema>,
87+
keyof TMethods
88+
>]: PrototypeMethods<TSchema>[Key]
89+
} & Except<TMethods, "update" | "getIdentifier">
90+
: never
91+
: PrototypeMethods<TSchema>
3592
```
36-
This is the type of values in the __*object*__.<br/>
37-
It indicates a function that takes Ż
38-
- `this` of type `TSchema` (the entity schema) as its first parameter. (We need to have an access to the object with data inside a function)
39-
- `..args: any[]` means any number of arguments of any type (`rest syntax`)
4093
41-
94+
The `ResolvedMethods` type uses conditional types to perform this check and manipulation. It leverages TypeScript's behavior with union types and conditional types to make these distinctions. 🤯
95+
96+
To have a better view we will breakdown `ResolvedMethods` step by step
97+
98+
#### 1. Generics
99+
- `TSchema` - which is a data layer for entity,
100+
- `TMethods` - which is an `object` of user-defined methods or `undefined`
101+
102+
#### 2. Conditionals for User-Defined Methods
103+
104+
```ts
105+
(
106+
TMethods extends Methods<TSchema>
107+
? TMethods extends undefined
108+
? "false"
109+
: "true"
110+
: 0
111+
)
112+
```
113+
It's a part where type need to check if user defined any methods. Type calculates correct input to proceed.
114+
115+
This type uses double check if `TMethods` are equal to `Methods` and to `undefined`.
116+
By this operation we are sure that we talking about non declared `TMethods` by user.
117+
Why? Because TypeScript can't explicit and clearly tell it's undefined - it uses union
118+
of undefined and the type used in extends check - so it's `Methods<TSchema> | undefined`.
119+
120+
When we detect state of not declared `TMethods` we are using string literal type "true" to
121+
mark it. TypeScript has Distributive Conditional Types mechanism it will check
122+
union `Methods<TSchema> | undefined` twice, and answer will be `false | 0`.
123+
124+
Such combination will without any problems rejected from next conditional type check for `"true"` literal.
125+
It's important here to combine two primitives types, not any other such as undefined or unknown.
126+
127+
The `0` is kinda random, we need to return anything but `"true"`.
128+
129+
The calculations will be always an union (Type is running mechanism twice for `Methods<TSchema>` & `undefined`)
130+
and result will contain `"true"` or `"false"` or `0`
131+
132+
#### 3. Augmentation of Methods
133+
```ts
134+
... extends "true"
135+
? TMethods extends Methods<TSchema>
136+
? {
137+
[Key in Exclude<
138+
keyof PrototypeMethods<TSchema>,
139+
keyof TMethods
140+
>]: PrototypeMethods<TSchema>[Key]
141+
} & Except<TMethods, "update" | "getIdentifier">
142+
: never
143+
: PrototypeMethods<TSchema>
144+
```
145+
146+
If Calculations from [Conditionals for User-Defined Methods](#conditionals-for-user-defined-methods) is labeled as `"true"` it means that the user has explicitly defined methods.\
147+
Type then checks if the user-defined methods (`TMethods`) are a subset of the prototype methods (`PrototypeMethods<TSchema>`).
148+
If they are, Type augments the user-defined methods with additional prototype methods, excluding specific keys (`update` and `getIdentifier`).
149+
150+
If Calculations from [Conditionals for User-Defined Methods](#conditionals-for-user-defined-methods) is labeled as `"false"` (meaning it's not explicitly defined), the type defaults to using the prototype methods (`PrototypeMethods<TSchema>`).
151+
152+
#### 4. Resulting Type:
153+
The resulting type is a combination of
154+
- prototype methods
155+
- user-defined methods (ensuring that the user-defined methods are properly augmented based on the prototype methods).

0 commit comments

Comments
 (0)