|
2 | 2 |
|
3 | 3 | In this section we will explore 🚀 all nooks and crannies behind methods types. |
4 | 4 |
|
| 5 | +## Methods |
5 | 6 |
|
| 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 |
6 | 10 |
|
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 |
16 | 11 |
|
| 12 | +### How? |
17 | 13 | ```ts |
18 | 14 | export type Methods<TSchema extends EntitySchema> = Record< |
19 | 15 | string, // keys |
20 | 16 | (this: TSchema, ...args: any[]) => any // values |
21 | 17 | > |
22 | 18 | ``` |
23 | 19 |
|
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 |
26 | 35 |
|
| 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. |
27 | 43 |
|
28 | 44 | ```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 | +} |
30 | 51 | ``` |
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? |
32 | 71 |
|
33 | 72 | ```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> |
35 | 92 | ``` |
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`) |
40 | 93 |
|
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