@penner/smart-primitive 0.0.1

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/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @penner/smart-primitive
2
+
3
+ Type-safe branded primitives with zero runtime overhead. Prevent bugs by distinguishing different kinds of numbers, strings, and booleans at compile time.
4
+
5
+ ## Why Smart Primitives?
6
+
7
+ Ever accidentally used milliseconds where pixels were expected? Or passed a URL to a function expecting a CSS selector? **Smart primitives** catch these mistakes at compile time, with zero runtime cost.
8
+
9
+ ```typescript
10
+ import { SmartNumber, SmartString } from '@penner/smart-primitive';
11
+
12
+ type Pixels = SmartNumber<'Pixels'>;
13
+ type Milliseconds = SmartNumber<'Milliseconds'>;
14
+ type URL = SmartString<'URL'>;
15
+ type CSSSelector = SmartString<'CSSSelector'>;
16
+
17
+ // ✅ Works perfectly
18
+ let width: Pixels = 300;
19
+ let delay: Milliseconds = 500;
20
+ let link: URL = 'https://example.com';
21
+ let selector: CSSSelector = '.button';
22
+
23
+ // ❌ TypeScript catches the mistake!
24
+ let oops: Pixels = delay; // Error: Type 'Milliseconds' is not assignable to type 'Pixels'
25
+ let wrong: URL = selector; // Error: Type 'CSSSelector' is not assignable to type 'URL'
26
+ ```
27
+
28
+ ## Features
29
+
30
+ - ✅ **Zero runtime overhead** - Pure TypeScript, no JavaScript generated
31
+ - ✅ **Works with plain values** - No wrapping or conversion needed
32
+ - ✅ **Prevents cross-domain mixing** - TypeScript stops you from mixing incompatible types
33
+ - ✅ **Toggleable type safety** - Turn off all smart typing with one flag
34
+ - ✅ **Utility functions** - `Unbrand`, `BaseOf`, `UnbrandFn` for working with branded types
35
+ - ✅ **Clean tooltips** - TypeScript shows clean type names, not implementation details
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @penner/smart-primitive
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### Smart Numbers
46
+
47
+ Perfect for units, measurements, or any numeric domain:
48
+
49
+ ```typescript
50
+ import { SmartNumber } from '@penner/smart-primitive';
51
+
52
+ type Pixels = SmartNumber<'Pixels'>;
53
+ type Milliseconds = SmartNumber<'Milliseconds'>;
54
+ type Degrees = SmartNumber<'Degrees'>;
55
+
56
+ function animate(distance: Pixels, duration: Milliseconds, rotation: Degrees) {
57
+ console.log(`Move ${distance}px over ${duration}ms, rotate ${rotation}°`);
58
+ }
59
+
60
+ animate(100, 500, 90); // ✅ works
61
+ animate(100, 500, 500); // ✅ works (but is it degrees or milliseconds? TypeScript doesn't know yet)
62
+
63
+ // But if you assign first:
64
+ let delay: Milliseconds = 500;
65
+ animate(100, delay, delay); // ❌ Error! Can't use Milliseconds where Degrees expected
66
+ ```
67
+
68
+ ### Smart Strings
69
+
70
+ Distinguish between different kinds of strings:
71
+
72
+ ```typescript
73
+ import { SmartString } from '@penner/smart-primitive';
74
+
75
+ type URL = SmartString<'URL'>;
76
+ type EmailAddress = SmartString<'EmailAddress'>;
77
+ type CSSSelector = SmartString<'CSSSelector'>;
78
+
79
+ function fetchData(endpoint: URL) {
80
+ // implementation
81
+ }
82
+
83
+ function sendEmail(address: EmailAddress) {
84
+ // implementation
85
+ }
86
+
87
+ let api: URL = 'https://api.example.com';
88
+ let email: EmailAddress = 'user@example.com';
89
+
90
+ fetchData(api); // ✅ works
91
+ fetchData(email); // ❌ Error! EmailAddress is not a URL
92
+ ```
93
+
94
+ ### Smart Booleans
95
+
96
+ Even booleans can benefit from type safety:
97
+
98
+ ```typescript
99
+ import { SmartBoolean } from '@penner/smart-primitive';
100
+
101
+ type IsVisible = SmartBoolean<'IsVisible'>;
102
+ type IsEnabled = SmartBoolean<'IsEnabled'>;
103
+
104
+ function toggleVisibility(visible: IsVisible) {
105
+ // implementation
106
+ }
107
+
108
+ let visible: IsVisible = true;
109
+ let enabled: IsEnabled = true;
110
+
111
+ toggleVisibility(visible); // ✅ works
112
+ toggleVisibility(enabled); // ❌ Error! IsEnabled is not IsVisible
113
+ ```
114
+
115
+ ## Advanced Usage
116
+
117
+ ### Utility Types
118
+
119
+ #### `Unbrand<T>`
120
+
121
+ Remove branding from types, converting them back to primitives:
122
+
123
+ ```typescript
124
+ import { Unbrand } from '@penner/smart-primitive';
125
+
126
+ type Config = {
127
+ width: Pixels;
128
+ duration: Milliseconds;
129
+ url: URL;
130
+ };
131
+
132
+ type PlainConfig = Unbrand<Config>;
133
+ // Result: { width: number; duration: number; url: string; }
134
+
135
+ const config: PlainConfig = {
136
+ width: 100,
137
+ duration: 500,
138
+ url: 'https://example.com',
139
+ };
140
+ ```
141
+
142
+ #### `BaseOf<T>`
143
+
144
+ Extract the base primitive type:
145
+
146
+ ```typescript
147
+ import { BaseOf } from '@penner/smart-primitive';
148
+
149
+ type PixelsBase = BaseOf<Pixels>; // number
150
+ type URLBase = BaseOf<URL>; // string
151
+ type IsVisibleBase = BaseOf<IsVisible>; // boolean
152
+ ```
153
+
154
+ #### `UnbrandFn<F>`
155
+
156
+ Unbrand function parameters:
157
+
158
+ ```typescript
159
+ import { UnbrandFn } from '@penner/smart-primitive';
160
+
161
+ function animate(distance: Pixels, duration: Milliseconds): void {
162
+ // implementation
163
+ }
164
+
165
+ type PlainAnimate = UnbrandFn<typeof animate>;
166
+ // Result: (distance: number, duration: number) => void
167
+ ```
168
+
169
+ ### Feature Flag: Toggle Type Safety
170
+
171
+ You can disable all smart type checking with a single flag. This is useful for:
172
+
173
+ - Performance testing
174
+ - Debugging type issues
175
+ - Gradual migration
176
+ - Bundle size optimization
177
+
178
+ ```typescript
179
+ // In your SmartPrimitive.ts file
180
+ export const USE_PLAIN_PRIMITIVES = true as const; // 👈 Change to true
181
+
182
+ // Now ALL smart types become plain primitives
183
+ type Pixels = SmartNumber<'Pixels'>; // becomes: number
184
+ type URL = SmartString<'URL'>; // becomes: string
185
+ type IsVisible = SmartBoolean<'IsVisible'>; // becomes: boolean
186
+
187
+ // All cross-brand assignments are now allowed
188
+ let width: Pixels = 100;
189
+ let delay: Milliseconds = 200;
190
+ width = delay; // ✅ Now allowed! (when flag is true)
191
+ ```
192
+
193
+ ## How It Works
194
+
195
+ Smart primitives use TypeScript's brand pattern (also called phantom types or nominal typing). The implementation is remarkably simple:
196
+
197
+ ```typescript
198
+ export type SmartPrimitive<
199
+ Base extends string | number | boolean | bigint | symbol,
200
+ BrandName extends string,
201
+ > = Base & { readonly __brand?: BrandName };
202
+ ```
203
+
204
+ The `__brand` property is:
205
+
206
+ - **Optional** - so plain primitives are assignable
207
+ - **Readonly** - prevents accidental modification
208
+ - **Never actually exists at runtime** - TypeScript-only, zero overhead
209
+
210
+ ## TypeScript Compatibility
211
+
212
+ Requires TypeScript 4.5 or higher.
213
+
214
+ ## Examples
215
+
216
+ ### Complex Object Structures
217
+
218
+ ```typescript
219
+ type AnimationConfig = {
220
+ timing: {
221
+ duration: Milliseconds;
222
+ delay: Milliseconds;
223
+ };
224
+ position: {
225
+ start: Pixels;
226
+ end: Pixels;
227
+ };
228
+ rotation: Degrees;
229
+ };
230
+
231
+ const config: AnimationConfig = {
232
+ timing: { duration: 1000, delay: 200 },
233
+ position: { start: 0, end: 500 },
234
+ rotation: 180,
235
+ };
236
+ ```
237
+
238
+ ### Function Safety
239
+
240
+ ```typescript
241
+ function moveElement(
242
+ element: HTMLElement,
243
+ distance: Pixels,
244
+ duration: Milliseconds,
245
+ ): void {
246
+ // TypeScript ensures you can't accidentally swap parameters
247
+ }
248
+
249
+ let dist: Pixels = 100;
250
+ let time: Milliseconds = 500;
251
+
252
+ moveElement(element, dist, time); // ✅ correct
253
+ moveElement(element, time, dist); // ❌ Error! Parameters swapped
254
+ ```
255
+
256
+ ## License
257
+
258
+ MIT
259
+
260
+ ## Related Packages
261
+
262
+ - [`@penner/easing`](https://www.npmjs.com/package/@penner/easing) - Modern Penner easing functions
263
+ - [`@penner/responsive-easing`](https://www.npmjs.com/package/@penner/responsive-easing) - Responsive motion design system
264
+
265
+ ## Contributing
266
+
267
+ Contributions welcome! Please read the [contributing guidelines](../../CONTRIBUTING.md) first.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 🔧 FEATURE FLAG: Toggle smart primitive type checking
3
+ * When false (default): SmartPrimitive types provide type safety
4
+ * When true: All SmartNumber, SmartString, etc. become plain primitives
5
+ *
6
+ * Use cases:
7
+ * - Performance testing (eliminate type overhead)
8
+ * - Debugging type issues
9
+ * - Gradual migration to/from smart primitives
10
+ * - Bundle size optimization
11
+ *
12
+ * How to use:
13
+ * 1. Change `false` to `true` below
14
+ * 2. Your entire codebase now treats Pixels, URLs, etc. as plain primitives
15
+ * 3. Cross-brand assignments that were errors become allowed
16
+ * 4. All type extraction utilities become no-ops
17
+ * 5. Change back to `false` to re-enable smart primitive type safety
18
+ */
19
+ export declare const USE_PLAIN_PRIMITIVES: false;
20
+ export type USE_PLAIN_PRIMITIVES_TYPE = typeof USE_PLAIN_PRIMITIVES;
21
+ /**
22
+ * Clean type extraction using the brand pattern
23
+ * Respects the USE_PLAIN_PRIMITIVES flag.
24
+ */
25
+ type BaseOf<T> = USE_PLAIN_PRIMITIVES_TYPE extends true ? T : T extends number & {
26
+ readonly __brand?: unknown;
27
+ } ? number : T extends string & {
28
+ readonly __brand?: unknown;
29
+ } ? string : T extends boolean & {
30
+ readonly __brand?: unknown;
31
+ } ? boolean : T;
32
+ /**
33
+ * Generic smart primitive type that provides **opt-in type safety** while staying flexible.
34
+ *
35
+ * **How it works:**
36
+ * - ✅ **Accepts plain values**: You can use regular numbers, strings, etc. directly
37
+ * - ✅ **Prevents cross-domain mixing**: TypeScript stops you from using pixels where milliseconds are expected
38
+ * - ✅ **Zero runtime cost**: No performance impact - it's just TypeScript magic
39
+ * - ✅ **Easy to disable**: Toggle `USE_PLAIN_PRIMITIVES` to turn off all smart typing
40
+ *
41
+ * **Example:**
42
+ * ```ts
43
+ * type Pixels = SmartPrimitive<number, 'Pixels'>;
44
+ * type Milliseconds = SmartPrimitive<number, 'Milliseconds'>;
45
+ *
46
+ * let width: Pixels = 300; // ✅ Plain number works
47
+ * let delay: Milliseconds = 500; // ✅ Plain number works
48
+ * let oops: Pixels = delay; // ❌ TypeScript error - caught the mistake!
49
+ * ```
50
+ *
51
+ * This prevents bugs like accidentally using milliseconds where pixels are expected,
52
+ * while keeping your code simple and readable.
53
+ */
54
+ export type SmartPrimitive<Base extends string | number | boolean | bigint | symbol, BrandName extends string> = USE_PLAIN_PRIMITIVES_TYPE extends true ? Base : Base & {
55
+ readonly __brand?: BrandName;
56
+ };
57
+ /**
58
+ * A smart number type for domain-specific numeric values like pixels, milliseconds, etc.
59
+ *
60
+ * **Why use this?** Prevents common bugs like mixing up different kinds of numbers:
61
+ * - Using milliseconds where pixels are expected
62
+ * - Passing a width value to a duration parameter
63
+ * - Confusing degrees with radians
64
+ *
65
+ * **How it works:**
66
+ * - ✅ Works with plain numbers: `let width: Pixels = 300`
67
+ * - ✅ Catches domain mix-ups: `let width: Pixels = duration` → TypeScript error
68
+ * - ✅ Zero runtime cost: Just TypeScript checking, no JavaScript overhead
69
+ *
70
+ * Built on `SmartPrimitive` - respects the USE_PLAIN_PRIMITIVES flag for easy toggling.
71
+ *
72
+ * **Example:**
73
+ * ```ts
74
+ * type Pixels = SmartNumber<'Pixels'>;
75
+ * type Milliseconds = SmartNumber<'Milliseconds'>;
76
+ *
77
+ * let width: Pixels = 300; // ✅ works
78
+ * let delay: Milliseconds = 500; // ✅ works
79
+ * let badAssign: Pixels = delay; // ❌ type error - caught the mistake!
80
+ * ```
81
+ *
82
+ * When USE_PLAIN_PRIMITIVES = false (default):
83
+ * - Full smart number system with type safety between different units
84
+ * - Plain numbers accepted seamlessly
85
+ * - Clean tooltip display
86
+ *
87
+ * When USE_PLAIN_PRIMITIVES = true:
88
+ * - All smart number types collapse to plain `number`
89
+ * - Zero runtime overhead, maximum performance
90
+ *
91
+ * Usage remains the same regardless of flag state:
92
+ * ```ts
93
+ * let distance: Pixels = 300; // ✅ always works
94
+ * let branded: Pixels = 300 as Pixels; // ✅ always works
95
+ * ```
96
+ */
97
+ export type SmartNumber<BrandName extends string> = SmartPrimitive<number, BrandName>;
98
+ /**
99
+ * A smart string type that accepts plain strings but maintains type identity.
100
+ * Perfect for things like URLs, CSS selectors, or other domain-specific strings.
101
+ *
102
+ * Usage:
103
+ * ```ts
104
+ * type URL = SmartString<'URL'>;
105
+ * type CSSSelector = SmartString<'CSSSelector'>;
106
+ *
107
+ * let url: URL = "https://example.com"; // ✅ works
108
+ * let selector: CSSSelector = ".my-class"; // ✅ works
109
+ * let badAssign: URL = selector; // ❌ type error
110
+ * ```
111
+ */
112
+ export type SmartString<BrandName extends string> = SmartPrimitive<string, BrandName>;
113
+ /**
114
+ * A smart boolean type that accepts plain booleans but maintains type identity.
115
+ * Useful for domain-specific flags or state indicators.
116
+ *
117
+ * Usage:
118
+ * ```ts
119
+ * type IsVisible = SmartBoolean<'IsVisible'>;
120
+ * type IsEnabled = SmartBoolean<'IsEnabled'>;
121
+ *
122
+ * let visible: IsVisible = true; // ✅ works
123
+ * let enabled: IsEnabled = false; // ✅ works
124
+ * let badAssign: IsVisible = enabled; // ❌ type error
125
+ * ```
126
+ */
127
+ export type SmartBoolean<BrandName extends string> = SmartPrimitive<boolean, BrandName>;
128
+ /**
129
+ * Simplified Unbrand using the generic brand pattern.
130
+ * Also respects the USE_PLAIN_PRIMITIVES flag.
131
+ *
132
+ * When USE_PLAIN_PRIMITIVES = true: This becomes a no-op (types pass through unchanged)
133
+ * When USE_PLAIN_PRIMITIVES = false: Full unbranding to base types
134
+ */
135
+ export type Unbrand<T> = USE_PLAIN_PRIMITIVES_TYPE extends true ? T : T extends number & {
136
+ readonly __brand?: unknown;
137
+ } ? number : T extends string & {
138
+ readonly __brand?: unknown;
139
+ } ? string : T extends boolean & {
140
+ readonly __brand?: unknown;
141
+ } ? boolean : T extends Record<string, unknown> ? {
142
+ [K in keyof T]: Unbrand<T[K]>;
143
+ } : T;
144
+ /**
145
+ * Given any function type F, produce a new function type whose
146
+ * arguments have been passed through Unbrand<…>, and whose return
147
+ * type you can also choose to unbrand (or leave alone).
148
+ */
149
+ export type UnbrandFn<F, UnbrandReturn extends boolean = false> = F extends (...args: infer A) => infer R ? A extends readonly unknown[] ? (...args: {
150
+ [K in keyof A]: Unbrand<A[K]>;
151
+ }) => UnbrandReturn extends true ? Unbrand<R> : R : never : never;
152
+ export type { BaseOf };
153
+ //# sourceMappingURL=SmartPrimitive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SmartPrimitive.d.ts","sourceRoot":"","sources":["../src/SmartPrimitive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,eAAO,MAAM,oBAAoB,EAAG,KAAc,CAAC;AACnD,MAAM,MAAM,yBAAyB,GAAG,OAAO,oBAAoB,CAAC;AAEpE;;;GAGG;AACH,KAAK,MAAM,CAAC,CAAC,IAAI,yBAAyB,SAAS,IAAI,GACnD,CAAC,GACD,CAAC,SAAS,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,GACN,CAAC,SAAS,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,GACN,CAAC,SAAS,OAAO,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAChD,OAAO,GACP,CAAC,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,CACxB,IAAI,SAAS,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,EACxD,SAAS,SAAS,MAAM,IACtB,yBAAyB,SAAS,IAAI,GACtC,IAAI,GACJ,IAAI,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,MAAM,WAAW,CAAC,SAAS,SAAS,MAAM,IAAI,cAAc,CAChE,MAAM,EACN,SAAS,CACV,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,WAAW,CAAC,SAAS,SAAS,MAAM,IAAI,cAAc,CAChE,MAAM,EACN,SAAS,CACV,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,CAAC,SAAS,SAAS,MAAM,IAAI,cAAc,CACjE,OAAO,EACP,SAAS,CACV,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,yBAAyB,SAAS,IAAI,GAC3D,CAAC,GAGD,CAAC,SAAS,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,GACN,CAAC,SAAS,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,GACN,CAAC,SAAS,OAAO,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAChD,OAAO,GAEP,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAEjC,CAAC,CAAC;AAEd;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,aAAa,SAAS,OAAO,GAAG,KAAK,IAAI,CAAC,SAAS,CAC1E,GAAG,IAAI,EAAE,MAAM,CAAC,KACb,MAAM,CAAC,GACR,CAAC,SAAS,SAAS,OAAO,EAAE,GAC1B,CACE,GAAG,IAAI,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,KACvC,aAAa,SAAS,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAChD,KAAK,GACP,KAAK,CAAC;AAGV,YAAY,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=SmartPrimitive.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SmartPrimitive.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/SmartPrimitive.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=!1;exports.USE_PLAIN_PRIMITIVES=e;
2
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/SmartPrimitive.ts"],"sourcesContent":["/**\n * 🔧 FEATURE FLAG: Toggle smart primitive type checking\n * When false (default): SmartPrimitive types provide type safety\n * When true: All SmartNumber, SmartString, etc. become plain primitives\n *\n * Use cases:\n * - Performance testing (eliminate type overhead)\n * - Debugging type issues\n * - Gradual migration to/from smart primitives\n * - Bundle size optimization\n *\n * How to use:\n * 1. Change `false` to `true` below\n * 2. Your entire codebase now treats Pixels, URLs, etc. as plain primitives\n * 3. Cross-brand assignments that were errors become allowed\n * 4. All type extraction utilities become no-ops\n * 5. Change back to `false` to re-enable smart primitive type safety\n */\n\n// 🔧 FEATURE FLAG: Both runtime constant AND compile-time type\nexport const USE_PLAIN_PRIMITIVES = false as const; // 👈 Change to `true` for plain primitives, `false` for branded types\nexport type USE_PLAIN_PRIMITIVES_TYPE = typeof USE_PLAIN_PRIMITIVES;\n\n/**\n * Clean type extraction using the brand pattern\n * Respects the USE_PLAIN_PRIMITIVES flag.\n */\ntype BaseOf<T> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? T // 🚫 Smart types disabled: type is already the base\n : T extends number & { readonly __brand?: unknown }\n ? number\n : T extends string & { readonly __brand?: unknown }\n ? string\n : T extends boolean & { readonly __brand?: unknown }\n ? boolean\n : T; // 🎯 Extract base type or return as-is\n\n/**\n * Generic smart primitive type that provides **opt-in type safety** while staying flexible.\n *\n * **How it works:**\n * - ✅ **Accepts plain values**: You can use regular numbers, strings, etc. directly\n * - ✅ **Prevents cross-domain mixing**: TypeScript stops you from using pixels where milliseconds are expected\n * - ✅ **Zero runtime cost**: No performance impact - it's just TypeScript magic\n * - ✅ **Easy to disable**: Toggle `USE_PLAIN_PRIMITIVES` to turn off all smart typing\n *\n * **Example:**\n * ```ts\n * type Pixels = SmartPrimitive<number, 'Pixels'>;\n * type Milliseconds = SmartPrimitive<number, 'Milliseconds'>;\n *\n * let width: Pixels = 300; // ✅ Plain number works\n * let delay: Milliseconds = 500; // ✅ Plain number works\n * let oops: Pixels = delay; // ❌ TypeScript error - caught the mistake!\n * ```\n *\n * This prevents bugs like accidentally using milliseconds where pixels are expected,\n * while keeping your code simple and readable.\n */\nexport type SmartPrimitive<\n Base extends string | number | boolean | bigint | symbol,\n BrandName extends string,\n> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? Base\n : Base & { readonly __brand?: BrandName };\n\n/**\n * A smart number type for domain-specific numeric values like pixels, milliseconds, etc.\n *\n * **Why use this?** Prevents common bugs like mixing up different kinds of numbers:\n * - Using milliseconds where pixels are expected\n * - Passing a width value to a duration parameter\n * - Confusing degrees with radians\n *\n * **How it works:**\n * - ✅ Works with plain numbers: `let width: Pixels = 300`\n * - ✅ Catches domain mix-ups: `let width: Pixels = duration` → TypeScript error\n * - ✅ Zero runtime cost: Just TypeScript checking, no JavaScript overhead\n *\n * Built on `SmartPrimitive` - respects the USE_PLAIN_PRIMITIVES flag for easy toggling.\n *\n * **Example:**\n * ```ts\n * type Pixels = SmartNumber<'Pixels'>;\n * type Milliseconds = SmartNumber<'Milliseconds'>;\n *\n * let width: Pixels = 300; // ✅ works\n * let delay: Milliseconds = 500; // ✅ works\n * let badAssign: Pixels = delay; // ❌ type error - caught the mistake!\n * ```\n *\n * When USE_PLAIN_PRIMITIVES = false (default):\n * - Full smart number system with type safety between different units\n * - Plain numbers accepted seamlessly\n * - Clean tooltip display\n *\n * When USE_PLAIN_PRIMITIVES = true:\n * - All smart number types collapse to plain `number`\n * - Zero runtime overhead, maximum performance\n *\n * Usage remains the same regardless of flag state:\n * ```ts\n * let distance: Pixels = 300; // ✅ always works\n * let branded: Pixels = 300 as Pixels; // ✅ always works\n * ```\n */\nexport type SmartNumber<BrandName extends string> = SmartPrimitive<\n number,\n BrandName\n>;\n\n/**\n * A smart string type that accepts plain strings but maintains type identity.\n * Perfect for things like URLs, CSS selectors, or other domain-specific strings.\n *\n * Usage:\n * ```ts\n * type URL = SmartString<'URL'>;\n * type CSSSelector = SmartString<'CSSSelector'>;\n *\n * let url: URL = \"https://example.com\"; // ✅ works\n * let selector: CSSSelector = \".my-class\"; // ✅ works\n * let badAssign: URL = selector; // ❌ type error\n * ```\n */\nexport type SmartString<BrandName extends string> = SmartPrimitive<\n string,\n BrandName\n>;\n\n/**\n * A smart boolean type that accepts plain booleans but maintains type identity.\n * Useful for domain-specific flags or state indicators.\n *\n * Usage:\n * ```ts\n * type IsVisible = SmartBoolean<'IsVisible'>;\n * type IsEnabled = SmartBoolean<'IsEnabled'>;\n *\n * let visible: IsVisible = true; // ✅ works\n * let enabled: IsEnabled = false; // ✅ works\n * let badAssign: IsVisible = enabled; // ❌ type error\n * ```\n */\nexport type SmartBoolean<BrandName extends string> = SmartPrimitive<\n boolean,\n BrandName\n>;\n\n/**\n * Simplified Unbrand using the generic brand pattern.\n * Also respects the USE_PLAIN_PRIMITIVES flag.\n *\n * When USE_PLAIN_PRIMITIVES = true: This becomes a no-op (types pass through unchanged)\n * When USE_PLAIN_PRIMITIVES = false: Full unbranding to base types\n */\nexport type Unbrand<T> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? T // 🚫 Smart types disabled: types pass through unchanged\n : // 🎯 Smart types enabled: extract base types and recurse\n // 1️⃣ If T has a brand, extract the base type (could be number, string, etc.)\n T extends number & { readonly __brand?: unknown }\n ? number\n : T extends string & { readonly __brand?: unknown }\n ? string\n : T extends boolean & { readonly __brand?: unknown }\n ? boolean\n : // 2️⃣ If T is an object (e.g. your params bag), recurse each field\n T extends Record<string, unknown>\n ? { [K in keyof T]: Unbrand<T[K]> }\n : // 3️⃣ Everything else stays the same\n T;\n\n/**\n * Given any function type F, produce a new function type whose\n * arguments have been passed through Unbrand<…>, and whose return\n * type you can also choose to unbrand (or leave alone).\n */\nexport type UnbrandFn<F, UnbrandReturn extends boolean = false> = F extends (\n ...args: infer A\n) => infer R\n ? A extends readonly unknown[]\n ? (\n ...args: { [K in keyof A]: Unbrand<A[K]> }\n ) => UnbrandReturn extends true ? Unbrand<R> : R\n : never\n : never;\n\n// Export the BaseOf utility for advanced users\nexport type { BaseOf };\n"],"names":["USE_PLAIN_PRIMITIVES"],"mappings":"gFAoBO,MAAMA,EAAuB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @penner/smart-primitive - Type-safe branded primitives with zero runtime overhead
3
+ *
4
+ * Prevent bugs by distinguishing different kinds of numbers, strings, and booleans
5
+ * at compile time, with no runtime cost.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { SmartNumber, SmartString } from '@penner/smart-primitive';
10
+ *
11
+ * type Pixels = SmartNumber<'Pixels'>;
12
+ * type Milliseconds = SmartNumber<'Milliseconds'>;
13
+ * type URL = SmartString<'URL'>;
14
+ *
15
+ * let width: Pixels = 300; // ✅ works
16
+ * let delay: Milliseconds = 500; // ✅ works
17
+ * let oops: Pixels = delay; // ❌ type error - caught!
18
+ * ```
19
+ */
20
+ export type { SmartPrimitive, SmartNumber, SmartString, SmartBoolean, Unbrand, UnbrandFn, USE_PLAIN_PRIMITIVES_TYPE, BaseOf, } from './SmartPrimitive';
21
+ export { USE_PLAIN_PRIMITIVES } from './SmartPrimitive';
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EACV,cAAc,EACd,WAAW,EACX,WAAW,EACX,YAAY,EACZ,OAAO,EACP,SAAS,EACT,yBAAyB,EACzB,MAAM,GACP,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,5 @@
1
+ const I = !1;
2
+ export {
3
+ I as USE_PLAIN_PRIMITIVES
4
+ };
5
+ //# sourceMappingURL=index.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.js","sources":["../src/SmartPrimitive.ts"],"sourcesContent":["/**\n * 🔧 FEATURE FLAG: Toggle smart primitive type checking\n * When false (default): SmartPrimitive types provide type safety\n * When true: All SmartNumber, SmartString, etc. become plain primitives\n *\n * Use cases:\n * - Performance testing (eliminate type overhead)\n * - Debugging type issues\n * - Gradual migration to/from smart primitives\n * - Bundle size optimization\n *\n * How to use:\n * 1. Change `false` to `true` below\n * 2. Your entire codebase now treats Pixels, URLs, etc. as plain primitives\n * 3. Cross-brand assignments that were errors become allowed\n * 4. All type extraction utilities become no-ops\n * 5. Change back to `false` to re-enable smart primitive type safety\n */\n\n// 🔧 FEATURE FLAG: Both runtime constant AND compile-time type\nexport const USE_PLAIN_PRIMITIVES = false as const; // 👈 Change to `true` for plain primitives, `false` for branded types\nexport type USE_PLAIN_PRIMITIVES_TYPE = typeof USE_PLAIN_PRIMITIVES;\n\n/**\n * Clean type extraction using the brand pattern\n * Respects the USE_PLAIN_PRIMITIVES flag.\n */\ntype BaseOf<T> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? T // 🚫 Smart types disabled: type is already the base\n : T extends number & { readonly __brand?: unknown }\n ? number\n : T extends string & { readonly __brand?: unknown }\n ? string\n : T extends boolean & { readonly __brand?: unknown }\n ? boolean\n : T; // 🎯 Extract base type or return as-is\n\n/**\n * Generic smart primitive type that provides **opt-in type safety** while staying flexible.\n *\n * **How it works:**\n * - ✅ **Accepts plain values**: You can use regular numbers, strings, etc. directly\n * - ✅ **Prevents cross-domain mixing**: TypeScript stops you from using pixels where milliseconds are expected\n * - ✅ **Zero runtime cost**: No performance impact - it's just TypeScript magic\n * - ✅ **Easy to disable**: Toggle `USE_PLAIN_PRIMITIVES` to turn off all smart typing\n *\n * **Example:**\n * ```ts\n * type Pixels = SmartPrimitive<number, 'Pixels'>;\n * type Milliseconds = SmartPrimitive<number, 'Milliseconds'>;\n *\n * let width: Pixels = 300; // ✅ Plain number works\n * let delay: Milliseconds = 500; // ✅ Plain number works\n * let oops: Pixels = delay; // ❌ TypeScript error - caught the mistake!\n * ```\n *\n * This prevents bugs like accidentally using milliseconds where pixels are expected,\n * while keeping your code simple and readable.\n */\nexport type SmartPrimitive<\n Base extends string | number | boolean | bigint | symbol,\n BrandName extends string,\n> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? Base\n : Base & { readonly __brand?: BrandName };\n\n/**\n * A smart number type for domain-specific numeric values like pixels, milliseconds, etc.\n *\n * **Why use this?** Prevents common bugs like mixing up different kinds of numbers:\n * - Using milliseconds where pixels are expected\n * - Passing a width value to a duration parameter\n * - Confusing degrees with radians\n *\n * **How it works:**\n * - ✅ Works with plain numbers: `let width: Pixels = 300`\n * - ✅ Catches domain mix-ups: `let width: Pixels = duration` → TypeScript error\n * - ✅ Zero runtime cost: Just TypeScript checking, no JavaScript overhead\n *\n * Built on `SmartPrimitive` - respects the USE_PLAIN_PRIMITIVES flag for easy toggling.\n *\n * **Example:**\n * ```ts\n * type Pixels = SmartNumber<'Pixels'>;\n * type Milliseconds = SmartNumber<'Milliseconds'>;\n *\n * let width: Pixels = 300; // ✅ works\n * let delay: Milliseconds = 500; // ✅ works\n * let badAssign: Pixels = delay; // ❌ type error - caught the mistake!\n * ```\n *\n * When USE_PLAIN_PRIMITIVES = false (default):\n * - Full smart number system with type safety between different units\n * - Plain numbers accepted seamlessly\n * - Clean tooltip display\n *\n * When USE_PLAIN_PRIMITIVES = true:\n * - All smart number types collapse to plain `number`\n * - Zero runtime overhead, maximum performance\n *\n * Usage remains the same regardless of flag state:\n * ```ts\n * let distance: Pixels = 300; // ✅ always works\n * let branded: Pixels = 300 as Pixels; // ✅ always works\n * ```\n */\nexport type SmartNumber<BrandName extends string> = SmartPrimitive<\n number,\n BrandName\n>;\n\n/**\n * A smart string type that accepts plain strings but maintains type identity.\n * Perfect for things like URLs, CSS selectors, or other domain-specific strings.\n *\n * Usage:\n * ```ts\n * type URL = SmartString<'URL'>;\n * type CSSSelector = SmartString<'CSSSelector'>;\n *\n * let url: URL = \"https://example.com\"; // ✅ works\n * let selector: CSSSelector = \".my-class\"; // ✅ works\n * let badAssign: URL = selector; // ❌ type error\n * ```\n */\nexport type SmartString<BrandName extends string> = SmartPrimitive<\n string,\n BrandName\n>;\n\n/**\n * A smart boolean type that accepts plain booleans but maintains type identity.\n * Useful for domain-specific flags or state indicators.\n *\n * Usage:\n * ```ts\n * type IsVisible = SmartBoolean<'IsVisible'>;\n * type IsEnabled = SmartBoolean<'IsEnabled'>;\n *\n * let visible: IsVisible = true; // ✅ works\n * let enabled: IsEnabled = false; // ✅ works\n * let badAssign: IsVisible = enabled; // ❌ type error\n * ```\n */\nexport type SmartBoolean<BrandName extends string> = SmartPrimitive<\n boolean,\n BrandName\n>;\n\n/**\n * Simplified Unbrand using the generic brand pattern.\n * Also respects the USE_PLAIN_PRIMITIVES flag.\n *\n * When USE_PLAIN_PRIMITIVES = true: This becomes a no-op (types pass through unchanged)\n * When USE_PLAIN_PRIMITIVES = false: Full unbranding to base types\n */\nexport type Unbrand<T> = USE_PLAIN_PRIMITIVES_TYPE extends true\n ? T // 🚫 Smart types disabled: types pass through unchanged\n : // 🎯 Smart types enabled: extract base types and recurse\n // 1️⃣ If T has a brand, extract the base type (could be number, string, etc.)\n T extends number & { readonly __brand?: unknown }\n ? number\n : T extends string & { readonly __brand?: unknown }\n ? string\n : T extends boolean & { readonly __brand?: unknown }\n ? boolean\n : // 2️⃣ If T is an object (e.g. your params bag), recurse each field\n T extends Record<string, unknown>\n ? { [K in keyof T]: Unbrand<T[K]> }\n : // 3️⃣ Everything else stays the same\n T;\n\n/**\n * Given any function type F, produce a new function type whose\n * arguments have been passed through Unbrand<…>, and whose return\n * type you can also choose to unbrand (or leave alone).\n */\nexport type UnbrandFn<F, UnbrandReturn extends boolean = false> = F extends (\n ...args: infer A\n) => infer R\n ? A extends readonly unknown[]\n ? (\n ...args: { [K in keyof A]: Unbrand<A[K]> }\n ) => UnbrandReturn extends true ? Unbrand<R> : R\n : never\n : never;\n\n// Export the BaseOf utility for advanced users\nexport type { BaseOf };\n"],"names":["USE_PLAIN_PRIMITIVES"],"mappings":"AAoBO,MAAMA,IAAuB;"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@penner/smart-primitive",
3
+ "version": "0.0.1",
4
+ "description": "Type-safe branded primitives with zero runtime overhead - prevent bugs by distinguishing different kinds of numbers, strings, and booleans",
5
+ "author": "Robert Penner <robert@robertpenner.com> (https://robertpenner.com)",
6
+ "homepage": "https://github.com/robertpenner/penner",
7
+ "repository": "github:robertpenner/penner",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "engines": {
13
+ "node": ">=20.17"
14
+ },
15
+ "keywords": [
16
+ "typescript",
17
+ "branded-types",
18
+ "type-safety",
19
+ "smart-types",
20
+ "phantom-types",
21
+ "zero-runtime",
22
+ "compile-time",
23
+ "units",
24
+ "type-checking",
25
+ "developer-experience"
26
+ ],
27
+ "main": "dist/index.cjs.js",
28
+ "module": "dist/index.es.js",
29
+ "types": "dist/index.d.ts",
30
+ "type": "module",
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "dev": "tsc && vite build --watch",
38
+ "build": "tsc && vite build",
39
+ "build:types": "dts-bundle-generator --config ./dts-bundle-generator.config.ts",
40
+ "lint:scripts": "eslint ./src --ext .ts",
41
+ "format:scripts": "prettier ./src --write",
42
+ "test": "vitest run --typecheck",
43
+ "test:watch": "vitest"
44
+ },
45
+ "dependencies": {},
46
+ "devDependencies": {
47
+ "@types/node": "^22.8.1",
48
+ "@typescript-eslint/eslint-plugin": "^8.11.0",
49
+ "@typescript-eslint/parser": "^8.11.0",
50
+ "@vitest/coverage-v8": "^3.2.2",
51
+ "dts-bundle-generator": "^9.5.1",
52
+ "eslint": "^9.13.0",
53
+ "eslint-config-prettier": "^9.1.0",
54
+ "eslint-plugin-prettier": "^5.2.1",
55
+ "prettier": "^3.3.3",
56
+ "tslib": "^2.8.0",
57
+ "tsx": "^4.20.3",
58
+ "typescript": "^5.8.3",
59
+ "vite": "^5.4.10",
60
+ "vite-plugin-dts": "^4.3.0",
61
+ "vite-tsconfig-paths": "^5.1.4",
62
+ "vitest": "^3.2.2"
63
+ }
64
+ }