mnemonica 0.9.99787 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/AGENTS.md +138 -0
- package/.ai/ARCHITECT.md +110 -0
- package/.ai/CODE.md +111 -0
- package/.ai/DEBUG.md +179 -0
- package/.ai/ONBOARDING.md +171 -0
- package/.ai/TACTICA-DEEP-DIVE.md +583 -0
- package/.ai/TACTICA-RULES.md +103 -0
- package/.ai/ask/AGENTS.md +115 -0
- package/.ai/async_init.md +94 -0
- package/.ai/orchestrator/AGENTS.md +158 -0
- package/.ai/rules/CODING.md +229 -0
- package/.ai/rules/CONTEXT-CONDENSING.md +50 -0
- package/.ai/rules/REMINDERS.md +63 -0
- package/.ai/rules-skill/async-constructors.md +206 -0
- package/.ai/rules-skill/code-style.md +95 -0
- package/.ai/rules-skill/contributing.md +192 -0
- package/.ai/rules-skill/define-patterns.md +96 -0
- package/.ai/rules-skill/ecosystem.md +140 -0
- package/.ai/rules-skill/error-system.md +56 -0
- package/.ai/rules-skill/hooks.md +46 -0
- package/.ai/rules-skill/instance-methods.md +60 -0
- package/.ai/rules-skill/lookup-typed.md +84 -0
- package/.ai/rules-skill/philosophy.md +231 -0
- package/.ai/rules-skill/proxy-architecture.md +63 -0
- package/.ai/rules-skill/testing.md +66 -0
- package/.ai/rules-skill/type-system.md +114 -0
- package/.ai/task-templates/new-feature.md +46 -0
- package/AGENTS.md +250 -0
- package/CONTRIBUTING.md +112 -0
- package/FOR_HUMANS.md +1391 -0
- package/README.md +306 -999
- package/SKILL.md +109 -0
- package/build/api/errors/exceptionConstructor.js +3 -2
- package/build/api/errors/index.d.ts +5 -1
- package/build/api/errors/index.js +1 -1
- package/build/api/errors/throwModificationError.d.ts +1 -19
- package/build/api/errors/throwModificationError.js +5 -3
- package/build/api/hooks/HookInvocation.d.ts +15 -0
- package/build/api/hooks/HookInvocation.js +38 -0
- package/build/api/hooks/flowCheckers.d.ts +3 -2
- package/build/api/hooks/flowCheckers.js +1 -1
- package/build/api/hooks/invokeHook.d.ts +2 -3
- package/build/api/hooks/invokeHook.js +12 -21
- package/build/api/hooks/registerHook.d.ts +2 -1
- package/build/api/hooks/registerHook.js +1 -1
- package/build/api/index.d.ts +5 -7
- package/build/api/types/InstanceCreator.d.ts +9 -30
- package/build/api/types/InstanceCreator.js +38 -18
- package/build/api/types/InstanceModificator.d.ts +2 -1
- package/build/api/types/InstanceModificator.js +1 -1
- package/build/api/types/Mnemosyne.d.ts +8 -24
- package/build/api/types/Mnemosyne.js +15 -8
- package/build/api/types/Props.d.ts +2 -2
- package/build/api/types/Props.js +7 -3
- package/build/api/types/TypeProxy.d.ts +17 -14
- package/build/api/types/TypeProxy.js +5 -6
- package/build/api/types/compileNewModificatorFunctionBody.d.ts +13 -1
- package/build/api/types/compileNewModificatorFunctionBody.js +1 -1
- package/build/api/types/createInstanceModificator.d.ts +3 -2
- package/build/api/types/createInstanceModificator.js +1 -1
- package/build/api/types/index.d.ts +3 -2
- package/build/api/types/index.js +91 -73
- package/build/api/utils/index.d.ts +5 -2
- package/build/api/utils/index.js +1 -1
- package/build/constants/index.js +1 -1
- package/build/descriptors/errors/index.js +1 -1
- package/build/descriptors/index.d.ts +1 -1
- package/build/descriptors/index.js +1 -1
- package/build/descriptors/types/index.js +20 -17
- package/build/index.d.ts +3 -3
- package/build/index.js +7 -4
- package/build/types/index.d.ts +100 -67
- package/build/types/index.js +1 -1
- package/build/utils/collectConstructors.js +1 -1
- package/build/utils/defineStackCleaner.js +1 -1
- package/build/utils/extract.js +1 -1
- package/build/utils/hop.js +1 -1
- package/build/utils/index.js +3 -2
- package/build/utils/merge.js +1 -1
- package/build/utils/parent.js +1 -1
- package/build/utils/parse.js +9 -4
- package/build/utils/pick.js +1 -1
- package/build/utils/toJSON.js +1 -1
- package/docs/ai-learning-trajectory.md +142 -0
- package/docs/async-constructors.md +273 -0
- package/docs/purpose.md +676 -0
- package/docs/tactica-pattern.md +147 -0
- package/docs/typed-lookup.md +157 -0
- package/docs/typeomatica.md +622 -0
- package/examples/AsyncNewTest.js +12 -0
- package/examples/ClassReName.js +23 -0
- package/examples/README.md +40 -0
- package/examples/v8bug.js +64 -0
- package/module/index.js +19 -11
- package/package.json +27 -8
- package/src/api/errors/exceptionConstructor.ts +271 -0
- package/src/api/errors/index.ts +138 -0
- package/src/api/errors/throwModificationError.ts +272 -0
- package/src/api/hooks/HookInvocation.ts +69 -0
- package/src/api/hooks/flowCheckers.ts +27 -0
- package/src/api/hooks/index.ts +6 -0
- package/src/api/hooks/invokeHook.ts +67 -0
- package/src/api/hooks/registerHook.ts +35 -0
- package/src/api/index.ts +32 -0
- package/src/api/types/InstanceCreator.ts +463 -0
- package/src/api/types/InstanceModificator.ts +35 -0
- package/src/api/types/Mnemosyne.ts +445 -0
- package/src/api/types/Props.ts +262 -0
- package/src/api/types/TypeProxy.ts +215 -0
- package/src/api/types/compileNewModificatorFunctionBody.ts +215 -0
- package/src/api/types/createInstanceModificator.ts +70 -0
- package/src/api/types/index.ts +556 -0
- package/src/api/utils/index.ts +313 -0
- package/src/constants/index.ts +141 -0
- package/src/descriptors/errors/index.ts +26 -0
- package/src/descriptors/index.ts +13 -0
- package/src/descriptors/types/index.ts +319 -0
- package/src/index.ts +319 -0
- package/src/types/index.ts +538 -0
- package/src/utils/collectConstructors.ts +83 -0
- package/src/utils/defineStackCleaner.ts +13 -0
- package/src/utils/extract.ts +30 -0
- package/src/utils/hop.ts +6 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/merge.ts +29 -0
- package/src/utils/parent.ts +42 -0
- package/src/utils/parse.ts +85 -0
- package/src/utils/pick.ts +36 -0
- package/src/utils/toJSON.ts +44 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# The mnemonica + tactica Type-Safe Pattern
|
|
2
|
+
|
|
3
|
+
> **Project-agnostic reference.** This document applies to every project using mnemonica with tactica.
|
|
4
|
+
> Read it when you are tempted to write `as unknown as` with mnemonica types.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [The Runtime Reality: mnemonica Is a Trie](#1-the-runtime-reality-mnemonica-is-a-trie)
|
|
11
|
+
2. [The TypeScript Gap](#2-the-typescript-gap)
|
|
12
|
+
3. [How tactica Bridges the Gap](#3-how-tactica-bridges-the-gap)
|
|
13
|
+
4. [Declaration Merging: The TypeScript Mechanism](#4-declaration-merging-the-typescript-mechanism)
|
|
14
|
+
5. [tsconfig.json Setup](#5-tsconfigjson-setup)
|
|
15
|
+
6. [How lookupTyped Works](#6-how-lookuptyped-works)
|
|
16
|
+
7. [Why Direct Import Fails](#7-why-direct-import-fails)
|
|
17
|
+
8. [The Epiphany: Zero-Cast Chaining](#8-the-epiphany-zero-cast-chaining)
|
|
18
|
+
9. [Common Mistakes](#9-common-mistakes)
|
|
19
|
+
10. [Cheat Sheet](#10-cheat-sheet)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. The Runtime Reality: mnemonica Is a Trie
|
|
24
|
+
|
|
25
|
+
At runtime, mnemonica creates a **Trie data structure** of constructors.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// You define a type
|
|
29
|
+
const RequestData = define('RequestData', function (this: {...}, data) {
|
|
30
|
+
Object.assign(this, data);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// You define a child type
|
|
34
|
+
const RouteData = RequestData.define('RouteData', function (this: {...}, data) {
|
|
35
|
+
Object.assign(this, data);
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
What exists at runtime:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
RequestData (constructor)
|
|
43
|
+
├── RouteData (sub-constructor, accessible as RequestData.RouteData)
|
|
44
|
+
│ ├── PageData (sub-constructor)
|
|
45
|
+
│ │ ├── RenderData (sub-constructor)
|
|
46
|
+
│ │ │ └── ResponseData (sub-constructor)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
When you call `new RequestData(data)`, mnemonica:
|
|
50
|
+
1. Creates an instance with the prototype chain set
|
|
51
|
+
2. Attaches every sub-constructor as a property on the instance
|
|
52
|
+
3. The instance itself can spawn children: `new requestData.RouteData(data)`
|
|
53
|
+
|
|
54
|
+
**This is runtime behavior.** It works in JavaScript without any TypeScript involvement.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. The TypeScript Gap
|
|
59
|
+
|
|
60
|
+
TypeScript does **not** know about the Trie structure by default.
|
|
61
|
+
|
|
62
|
+
When you write:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { RequestData } from './collections/requestTypes.js';
|
|
66
|
+
const requestData = new RequestData({ method: 'GET', url: '/' });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
TypeScript sees `RequestData` as a constructor function returned by `define()`. It knows the return type is `RequestData` (the instance type), but it does **not** know that:
|
|
70
|
+
- `RequestData` has a `.RouteData` property
|
|
71
|
+
- The instance `requestData` has a `.RouteData` sub-constructor
|
|
72
|
+
- `.RouteData` returns an instance with `.PageData`
|
|
73
|
+
- And so on through the chain
|
|
74
|
+
|
|
75
|
+
So when you write:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const routeData = new requestData.RouteData({ pagePath: '/' });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
TypeScript complains: `Property 'RouteData' does not exist on type 'RequestData'`.
|
|
82
|
+
|
|
83
|
+
This is the gap: **mnemonica knows about the Trie, but TypeScript doesn't.**
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. How tactica Bridges the Gap
|
|
88
|
+
|
|
89
|
+
tactica is an AST analyzer. It scans your codebase for `define()` calls and understands:
|
|
90
|
+
- The type name (`'RequestData'`)
|
|
91
|
+
- The parent type (if any)
|
|
92
|
+
- The `this` properties
|
|
93
|
+
- The constructor parameters
|
|
94
|
+
- The full inheritance chain
|
|
95
|
+
|
|
96
|
+
It generates two files in `.tactica/`:
|
|
97
|
+
|
|
98
|
+
### 3.1 `.tactica/types.ts`
|
|
99
|
+
|
|
100
|
+
Contains the instance types:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
export type RequestData = {
|
|
104
|
+
method: string;
|
|
105
|
+
url: string;
|
|
106
|
+
query: Record<string, unknown>;
|
|
107
|
+
// ...
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type RequestData_RouteData = ProtoFlat<RequestData, {
|
|
111
|
+
pagePath: string;
|
|
112
|
+
isMain: boolean;
|
|
113
|
+
deep: string;
|
|
114
|
+
}>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`ProtoFlat<A, B>` merges the properties of parent `A` and child `B`.
|
|
118
|
+
|
|
119
|
+
### 3.2 `.tactica/registry.ts`
|
|
120
|
+
|
|
121
|
+
Contains the **TypeRegistry augmentation**:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
declare module 'mnemonica' {
|
|
125
|
+
interface TypeRegistry {
|
|
126
|
+
'RequestData': new (...args: unknown[]) => RequestData;
|
|
127
|
+
'RequestData_RouteData': new (...args: unknown[]) => RequestData_RouteData;
|
|
128
|
+
'RequestData_RouteData_PageData': new (...args: unknown[]) => RequestData_RouteData_PageData;
|
|
129
|
+
// ... every type in every chain
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**This is the critical file.** It teaches TypeScript about the constructors.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Declaration Merging: The TypeScript Mechanism
|
|
139
|
+
|
|
140
|
+
To understand why `.tactica/registry.ts` works, you need to understand **TypeScript declaration merging**.
|
|
141
|
+
|
|
142
|
+
### 4.1 What Is Declaration Merging?
|
|
143
|
+
|
|
144
|
+
TypeScript allows multiple declarations with the same name to be **merged** into a single definition:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// File A.ts
|
|
148
|
+
interface Person {
|
|
149
|
+
name: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// File B.ts
|
|
153
|
+
interface Person {
|
|
154
|
+
age: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Result: Person has both name and age
|
|
158
|
+
const p: Person = { name: 'Alice', age: 30 }; // OK
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This works for:
|
|
162
|
+
- Interfaces (merged member-wise)
|
|
163
|
+
- Namespaces (merged member-wise)
|
|
164
|
+
- Classes with namespaces
|
|
165
|
+
- **Module augmentations** ← this is what tactica uses
|
|
166
|
+
|
|
167
|
+
### 4.2 Module Augmentation
|
|
168
|
+
|
|
169
|
+
You can augment an existing module's exports using `declare module`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Original module
|
|
173
|
+
// node_modules/some-lib/index.d.ts
|
|
174
|
+
export interface Config {
|
|
175
|
+
timeout: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Your augmentation
|
|
179
|
+
// src/augmentation.ts
|
|
180
|
+
declare module 'some-lib' {
|
|
181
|
+
interface Config {
|
|
182
|
+
retries: number;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Now Config has both timeout and retries
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 4.3 How tactica Uses It
|
|
190
|
+
|
|
191
|
+
mnemonica core defines:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// In mnemonica core (src/types/index.ts)
|
|
195
|
+
export interface TypeRegistry {
|
|
196
|
+
[key: string]: never; // Empty by default, prevents accidental usage
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
tactica generates:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// In .tactica/registry.ts
|
|
204
|
+
declare module 'mnemonica' {
|
|
205
|
+
interface TypeRegistry {
|
|
206
|
+
'RequestData': new (...args: unknown[]) => RequestData;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
After this augmentation, `TypeRegistry` contains the `'RequestData'` key. Any code that imports from `'mnemonica'` sees the augmented `TypeRegistry`.
|
|
212
|
+
|
|
213
|
+
### 4.4 Why `[key: string]: never`?
|
|
214
|
+
|
|
215
|
+
The `[key: string]: never` pattern in the base `TypeRegistry` is a TypeScript trick:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
interface TypeRegistry {
|
|
219
|
+
[key: string]: never;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
type Test = TypeRegistry['anything']; // Error: Type 'anything' is not assignable to type 'never'
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This means **you cannot use `TypeRegistry` without augmentation**. It forces you to run tactica and generate the registry. Without it, any `lookupTyped('Something')` would be a compile error.
|
|
226
|
+
|
|
227
|
+
Once augmented:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
declare module 'mnemonica' {
|
|
231
|
+
interface TypeRegistry {
|
|
232
|
+
'RequestData': new (...args: unknown[]) => RequestData;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type Test = TypeRegistry['RequestData']; // OK — returns the constructor type
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 5. tsconfig.json Setup
|
|
242
|
+
|
|
243
|
+
For declaration merging to work, the `.tactica/registry.ts` file must be included in your TypeScript compilation.
|
|
244
|
+
|
|
245
|
+
### 5.1 Required Configuration
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"compilerOptions": {
|
|
250
|
+
"module": "NodeNext",
|
|
251
|
+
"moduleResolution": "NodeNext",
|
|
252
|
+
"strict": true,
|
|
253
|
+
"esModuleInterop": true
|
|
254
|
+
},
|
|
255
|
+
"include": [
|
|
256
|
+
"src/**/*",
|
|
257
|
+
".tactica/**/*"
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Critical points:**
|
|
263
|
+
|
|
264
|
+
1. **`.tactica` must be in `include`** — Otherwise TypeScript never sees the augmentation.
|
|
265
|
+
|
|
266
|
+
2. **`strict: true`** — Ensures the type system is fully active.
|
|
267
|
+
|
|
268
|
+
3. **`moduleResolution: "NodeNext"`** — Required for modern ESM resolution with `.js` extensions.
|
|
269
|
+
|
|
270
|
+
### 5.2 Why `include` Matters
|
|
271
|
+
|
|
272
|
+
TypeScript only processes files in the `include` array. If `.tactica/` is not included:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { lookupTyped } from 'mnemonica';
|
|
276
|
+
const RequestData = lookupTyped('RequestData');
|
|
277
|
+
// Error: 'RequestData' is not assignable to parameter of type 'never'
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Because without the augmentation, `TypeRegistry` still has `[key: string]: never`.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 6. How lookupTyped Works
|
|
285
|
+
|
|
286
|
+
### 6.1 Runtime Behavior
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// In mnemonica core (src/index.ts)
|
|
290
|
+
export const lookupTyped = function <const K extends keyof TypeRegistry>(
|
|
291
|
+
TypeNestedPath: K
|
|
292
|
+
): TypeRegistry[K] {
|
|
293
|
+
// Runtime delegates to lookup()
|
|
294
|
+
return types.lookup(TypeNestedPath as string) as TypeRegistry[K];
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
At runtime, `lookupTyped('RequestData')`:
|
|
299
|
+
1. Calls `types.lookup('RequestData')`
|
|
300
|
+
2. Searches the default types collection
|
|
301
|
+
3. Returns the constructor function
|
|
302
|
+
|
|
303
|
+
**This is the same constructor as the direct import.** No difference at runtime.
|
|
304
|
+
|
|
305
|
+
### 6.2 Compile-Time Behavior
|
|
306
|
+
|
|
307
|
+
At compile time, TypeScript sees:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
const RequestData = lookupTyped('RequestData');
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
TypeScript infers:
|
|
314
|
+
- `K` = `'RequestData'` (literal type, thanks to `const`)
|
|
315
|
+
- `keyof TypeRegistry` = all registered type names
|
|
316
|
+
- `TypeRegistry['RequestData']` = `new (...args: unknown[]) => RequestData`
|
|
317
|
+
|
|
318
|
+
So `RequestData` has type `new (...args: unknown[]) => RequestData`.
|
|
319
|
+
|
|
320
|
+
When you then write:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
const requestData = new RequestData({ method: 'GET', url: '/' });
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
TypeScript knows `requestData` is of type `RequestData` (the instance type).
|
|
327
|
+
|
|
328
|
+
### 6.3 The Constructor Has Sub-Constructors
|
|
329
|
+
|
|
330
|
+
But here's the key: the `.tactica/types.ts` file also defines the instance type to have sub-constructors:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// This is part of how ProtoFlat and the generated types work
|
|
334
|
+
// The RequestData instance type KNOWS it has .RouteData
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
When TypeScript sees:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const routeData = new requestData.RouteData({ pagePath: '/' });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
It checks the type of `requestData` → `RequestData`.
|
|
344
|
+
It checks if `RequestData` has `.RouteData` → Yes, in the generated type.
|
|
345
|
+
It checks the constructor signature → `new (...args: unknown[]) => RequestData_RouteData`.
|
|
346
|
+
|
|
347
|
+
**No cast needed.** TypeScript believes you because the type system was told the truth.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 7. Why Direct Import Fails
|
|
352
|
+
|
|
353
|
+
Let's trace what happens with direct import vs `lookupTyped`.
|
|
354
|
+
|
|
355
|
+
### 7.1 Direct Import
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import { RequestData } from './collections/requestTypes.js';
|
|
359
|
+
// ↑
|
|
360
|
+
// TypeScript sees: typeof RequestData (the constructor)
|
|
361
|
+
// It knows: new (...args: unknown[]) => RequestData
|
|
362
|
+
// It does NOT know: RequestData.RouteData exists
|
|
363
|
+
|
|
364
|
+
const requestData = new RequestData({ ... });
|
|
365
|
+
// TypeScript: requestData is RequestData
|
|
366
|
+
|
|
367
|
+
const routeData = new requestData.RouteData({ ... });
|
|
368
|
+
// TypeScript ERROR: Property 'RouteData' does not exist on type 'RequestData'
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The direct import returns the constructor, but TypeScript's type for that constructor doesn't include `.RouteData`. The `define()` function's return type is not augmented with sub-constructor information.
|
|
372
|
+
|
|
373
|
+
### 7.2 lookupTyped
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { lookupTyped } from 'mnemonica';
|
|
377
|
+
|
|
378
|
+
const RequestData = lookupTyped('RequestData');
|
|
379
|
+
// ↑
|
|
380
|
+
// TypeScript sees: TypeRegistry['RequestData']
|
|
381
|
+
// Which is: new (...args: unknown[]) => RequestData
|
|
382
|
+
// PLUS the instance type RequestData knows about .RouteData
|
|
383
|
+
|
|
384
|
+
const requestData = new RequestData({ ... });
|
|
385
|
+
// TypeScript: requestData is RequestData
|
|
386
|
+
|
|
387
|
+
const routeData = new requestData.RouteData({ ... });
|
|
388
|
+
// TypeScript: OK! RouteData is a known property of RequestData instance
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The difference is not in the runtime object. The difference is in **TypeScript's compile-time knowledge**.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 8. The Epiphany: Zero-Cast Chaining
|
|
396
|
+
|
|
397
|
+
The complete pattern, end to end:
|
|
398
|
+
|
|
399
|
+
### Step 1: Define
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// src/collections/requestTypes.ts
|
|
403
|
+
import { define } from 'mnemonica';
|
|
404
|
+
|
|
405
|
+
export const RequestData = define('RequestData', function (
|
|
406
|
+
this: { method: string; url: string },
|
|
407
|
+
data: { method: string; url: string }
|
|
408
|
+
) {
|
|
409
|
+
Object.assign(this, data);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
export const RouteData = RequestData.define('RouteData', function (
|
|
413
|
+
this: { pagePath: string },
|
|
414
|
+
data: { pagePath: string }
|
|
415
|
+
) {
|
|
416
|
+
Object.assign(this, data);
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Step 2: Generate
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npx tactica --esm --verbose
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Creates:
|
|
427
|
+
- `.tactica/types.ts` — instance types
|
|
428
|
+
- `.tactica/registry.ts` — TypeRegistry augmentation
|
|
429
|
+
|
|
430
|
+
### Step 3: Lookup
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// src/server.ts or any file
|
|
434
|
+
import { lookupTyped } from 'mnemonica';
|
|
435
|
+
|
|
436
|
+
const RequestData = lookupTyped('RequestData');
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Step 4: Chain (Zero Casts)
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const requestData = new RequestData({ method: 'GET', url: '/' });
|
|
443
|
+
const routeData = new requestData.RouteData({ pagePath: '/' });
|
|
444
|
+
const pageData = new routeData.PageData({ header: {...}, content: '' });
|
|
445
|
+
// ... and so on
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Every `.SubType` access is fully typed.** TypeScript knows:
|
|
449
|
+
- The constructor signature
|
|
450
|
+
- The instance properties
|
|
451
|
+
- The next sub-constructor in the chain
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 9. Common Mistakes
|
|
456
|
+
|
|
457
|
+
### 9.1 "I'll just cast it"
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// ❌ WRONG
|
|
461
|
+
const requestData = new RequestData({ ... }) as unknown as RequestDataT;
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Why wrong:** You are fighting the type system instead of using it. Every cast is a bug waiting to happen. If the type changes, the cast still compiles but may break at runtime.
|
|
465
|
+
|
|
466
|
+
**Fix:** Use `lookupTyped`.
|
|
467
|
+
|
|
468
|
+
### 9.2 "I'll import the generated types too"
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// ❌ WRONG
|
|
472
|
+
import { RequestData } from './collections/requestTypes.js';
|
|
473
|
+
import type { RequestData as RequestDataT } from '../../.tactica/types.js';
|
|
474
|
+
const requestData = new RequestData({ ... }) as unknown as RequestDataT;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Why wrong:** You are importing both the runtime constructor AND the generated type, then bridging them with a cast. This is twice the work and still unsafe.
|
|
478
|
+
|
|
479
|
+
**Fix:** Use `lookupTyped` — it gives you both the runtime constructor AND the type in one call.
|
|
480
|
+
|
|
481
|
+
### 9.3 "lookupTyped only works in route handlers"
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// ❌ WRONG (unnecessary)
|
|
485
|
+
app.get('/test', async () => {
|
|
486
|
+
const RequestData = lookupTyped('RequestData');
|
|
487
|
+
const requestData = new RequestData({ ... });
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Why wrong:** `lookupTyped` is a runtime lookup, but it's deterministic and cached. Calling it at module level is perfectly fine and more efficient:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// ✅ CORRECT
|
|
495
|
+
const RequestData = lookupTyped('RequestData');
|
|
496
|
+
|
|
497
|
+
app.get('/test', async () => {
|
|
498
|
+
const requestData = new RequestData({ ... });
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 9.4 "I need to import the constructor for decoration"
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// ❌ WRONG (mixing patterns)
|
|
506
|
+
import { RequestData } from './collections/requestTypes.js';
|
|
507
|
+
const TypedRequestData = lookupTyped('RequestData');
|
|
508
|
+
|
|
509
|
+
app.decorate('RequestData', RequestData); // direct import
|
|
510
|
+
const requestData = new TypedRequestData({ ... }); // lookupTyped
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Why wrong:** You're maintaining two references to the same object.
|
|
514
|
+
|
|
515
|
+
**Fix:** Use `lookupTyped` for everything. The returned constructor is the same object:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// ✅ CORRECT
|
|
519
|
+
const RequestData = lookupTyped('RequestData');
|
|
520
|
+
|
|
521
|
+
app.decorate('RequestData', RequestData);
|
|
522
|
+
const requestData = new RequestData({ ... });
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### 9.5 Forgetting to regenerate
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// You add a new property to the define() call
|
|
529
|
+
// But forget to run tactica
|
|
530
|
+
const RequestData = lookupTyped('RequestData');
|
|
531
|
+
const requestData = new RequestData({ newField: 'value' });
|
|
532
|
+
// Error: Object literal may only specify known properties
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Fix:** Always regenerate after modifying `define()` calls:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
npm run tactica
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## 10. Cheat Sheet
|
|
544
|
+
|
|
545
|
+
| I want to... | Do this | Don't do this |
|
|
546
|
+
|---|---|---|
|
|
547
|
+
| Get a typed constructor | `const T = lookupTyped('T')` | `import { T } from './collections/T.js'` |
|
|
548
|
+
| Create an instance | `new T({ ... })` | `new T({ ... }) as unknown as TT` |
|
|
549
|
+
| Chain to a child type | `new instance.Child({ ... })` | `new (instance as any).Child({ ... })` |
|
|
550
|
+
| Decorate Fastify | `app.decorate('T', T)` with `lookupTyped` | Direct import + separate lookupTyped |
|
|
551
|
+
| Add a new property | Modify `define()` → run `tactica` | Modify `define()` + manual cast |
|
|
552
|
+
| Fix "Property does not exist" | Run `tactica` to regenerate | Add `as any` or `as unknown` |
|
|
553
|
+
|
|
554
|
+
### Build Commands
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
# Regenerate types after modifying define() calls
|
|
558
|
+
npm run tactica
|
|
559
|
+
|
|
560
|
+
# Verify everything compiles
|
|
561
|
+
npx tsc --noEmit
|
|
562
|
+
|
|
563
|
+
# Verify linting
|
|
564
|
+
npx eslint src/
|
|
565
|
+
|
|
566
|
+
# Run tests
|
|
567
|
+
npx vitest run
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Trust
|
|
573
|
+
|
|
574
|
+
This pattern works because:
|
|
575
|
+
|
|
576
|
+
1. **mnemonica creates a Trie at runtime** — sub-constructors exist, instances can spawn children
|
|
577
|
+
2. **tactica teaches TypeScript about the Trie** — via declaration merging of `TypeRegistry`
|
|
578
|
+
3. **lookupTyped retrieves the typed constructor** — compile-time type safety + runtime correctness
|
|
579
|
+
4. **The same object is returned either way** — `import { T }` and `lookupTyped('T')` are identical at runtime
|
|
580
|
+
|
|
581
|
+
**The only difference is TypeScript's knowledge.** Use `lookupTyped` and let TypeScript help you.
|
|
582
|
+
|
|
583
|
+
If you find yourself writing `as unknown as` with mnemonica types, **you have taken a wrong turn.** Stop. Use `lookupTyped`. Trust the registry.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# mnemonica + `lookupTyped`: rules and the underlying pattern
|
|
2
|
+
|
|
3
|
+
> **The real rule:** use `lookupTyped()` plus an augmented `TypeRegistry` — never `import { X } from './collections/...'` plus `as unknown as` casts. The augmentation can be hand-written or generated by [`@mnemonica/tactica`](https://www.npmjs.com/package/@mnemonica/tactica). Tactica is the productivity tool; the **augmentation** is the requirement. For the full mechanism and a side-by-side example, see [`../docs/typed-lookup.md`](../docs/typed-lookup.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What this file is
|
|
8
|
+
|
|
9
|
+
The anti-patterns that show up when working with a mnemonica project — and what to do instead. The rules apply regardless of whether you use tactica or hand-augment `TypeRegistry`; the runtime is identical either way.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why the type-safe path matters
|
|
14
|
+
|
|
15
|
+
mnemonica creates a Trie of constructors at runtime:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
RequestData
|
|
19
|
+
└── RouteData
|
|
20
|
+
└── PageData
|
|
21
|
+
└── RenderData
|
|
22
|
+
└── ResponseData
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
At runtime, this works perfectly — `new requestData.RouteData({ ... })` just works.
|
|
26
|
+
|
|
27
|
+
But TypeScript does not see this Trie. The `define()` function returns a constructor TypeScript understands as a single function — not as something that grows `.RouteData`, `.PageData`, etc. on each instance.
|
|
28
|
+
|
|
29
|
+
The temptation is to cast around the gap:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// ❌ never
|
|
33
|
+
const requestData = new RequestData({ ... }) as unknown as RequestDataT;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That bypasses the type system. The right answer is to teach TypeScript what the Trie looks like — which is exactly what `lookupTyped()` + a `TypeRegistry` augmentation does.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## The mechanism in one paragraph
|
|
41
|
+
|
|
42
|
+
`mnemonica` exports `interface TypeRegistry { [key: string]: TypeConstructor<never> }`. `lookupTyped<K extends keyof TypeRegistry>(path: K)` returns `TypeRegistry[K]`. So a `declare module 'mnemonica' { interface TypeRegistry { ... } }` augmentation in your project — by hand, or generated by tactica — gives `lookupTyped('Path.To.Type')` a fully typed return value. Zero casts.
|
|
43
|
+
|
|
44
|
+
After augmentation, TypeScript knows:
|
|
45
|
+
- The constructor arguments for each type
|
|
46
|
+
- The instance properties
|
|
47
|
+
- The sub-constructors on each instance (`.RouteData`, `.PageData`, etc.)
|
|
48
|
+
|
|
49
|
+
See [`../docs/typed-lookup.md`](../docs/typed-lookup.md) for both paths with examples.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## When to insist on what
|
|
54
|
+
|
|
55
|
+
When asked to work with a mnemonica project:
|
|
56
|
+
|
|
57
|
+
1. **Check whether `TypeRegistry` is augmented somewhere.** This can be:
|
|
58
|
+
- A `.tactica/` directory with `registry.ts` (tactica-generated), OR
|
|
59
|
+
- A `.d.ts` or `.ts` file containing `declare module 'mnemonica' { interface TypeRegistry { ... } }` (hand-written)
|
|
60
|
+
|
|
61
|
+
If neither exists, the project has no type-safe path. Ask the user before writing code: do they want tactica set up, or to hand-augment? Either is fine — but pick one before writing types.
|
|
62
|
+
|
|
63
|
+
2. **If `.tactica/` exists, check `tsconfig.json` includes it.** If missing, add `.tactica/**/*.ts` to `include`.
|
|
64
|
+
|
|
65
|
+
3. **Check that `lookupTyped` is being used.** Direct imports from collection files + casts are the smell that augmentation is missing or unused.
|
|
66
|
+
|
|
67
|
+
4. **Never accept `as unknown as` as a solution.** It is a symptom of the augmentation being missing, not a fix.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick reference
|
|
72
|
+
|
|
73
|
+
| Situation | Wrong | Right |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| Get a constructor | `import { T } from './collections/T.js'` | `const T = lookupTyped('T')` |
|
|
76
|
+
| Create instance | `new T({ ... }) as unknown as TT` | `new T({ ... })` |
|
|
77
|
+
| Chain to child | `new (instance as any).Child({ ... })` | `new instance.Child({ ... })` |
|
|
78
|
+
| Type not found | Add `as any` | Augment `TypeRegistry` (manually or run `tactica`) |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Build verification
|
|
83
|
+
|
|
84
|
+
After any change:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run tactica # only if using tactica — regenerate .tactica/
|
|
88
|
+
npx tsc --noEmit # verify zero TS errors
|
|
89
|
+
npx eslint src/ # verify zero lint errors
|
|
90
|
+
npm test # verify all tests pass
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If you are not using tactica, `npm run tactica` is not required; the hand-written augmentation is the source of truth.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Deeper reading
|
|
98
|
+
|
|
99
|
+
- [`../docs/typed-lookup.md`](../docs/typed-lookup.md) — `lookupTyped` with or without tactica (canonical reference, side-by-side)
|
|
100
|
+
- [`../docs/tactica-pattern.md`](../docs/tactica-pattern.md) — human-facing explanation of declaration merging
|
|
101
|
+
- [`./TACTICA-DEEP-DIVE.md`](./TACTICA-DEEP-DIVE.md) — comprehensive technical guide
|
|
102
|
+
|
|
103
|
+
The key insight: **the runtime constructor is identical whether you import it directly or look it up. The only difference is TypeScript's compile-time knowledge.** `lookupTyped` + augmented `TypeRegistry` teaches TypeScript what mnemonica already knows at runtime — and the augmentation can come from anywhere.
|