@penner/smart-primitive 0.0.1 → 0.1.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.
Files changed (81) hide show
  1. package/README.md +215 -180
  2. package/dist/SmartPrimitive.d.ts +87 -37
  3. package/dist/SmartPrimitive.d.ts.map +1 -1
  4. package/dist/SmartPrimitive.test-d.d.ts.map +1 -0
  5. package/dist/SmartPrimitiveConfig.test-d.d.ts +2 -0
  6. package/dist/SmartPrimitiveConfig.test-d.d.ts.map +1 -0
  7. package/dist/convert.cjs.js +2 -0
  8. package/dist/convert.cjs.js.map +1 -0
  9. package/dist/convert.d.ts +136 -0
  10. package/dist/convert.d.ts.map +1 -0
  11. package/dist/convert.es.js +51 -0
  12. package/dist/convert.es.js.map +1 -0
  13. package/dist/convert.test-d.d.ts +2 -0
  14. package/dist/convert.test-d.d.ts.map +1 -0
  15. package/dist/index.cjs.js +1 -1
  16. package/dist/index.cjs.js.map +1 -1
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.es.js +65 -2
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/strings/css.d.ts +143 -0
  22. package/dist/strings/css.d.ts.map +1 -0
  23. package/dist/strings/data.d.ts +140 -0
  24. package/dist/strings/data.d.ts.map +1 -0
  25. package/dist/strings/index.cjs.js +2 -0
  26. package/dist/strings/index.cjs.js.map +1 -0
  27. package/dist/strings/index.d.ts +14 -0
  28. package/dist/strings/index.d.ts.map +1 -0
  29. package/dist/strings/index.es.js +2 -0
  30. package/dist/strings/index.es.js.map +1 -0
  31. package/dist/strings/web.d.ts +55 -0
  32. package/dist/strings/web.d.ts.map +1 -0
  33. package/dist/trait-utils.d.ts +107 -0
  34. package/dist/trait-utils.d.ts.map +1 -0
  35. package/dist/traits.d.ts +14 -0
  36. package/dist/traits.d.ts.map +1 -0
  37. package/dist/traits.examples.d.ts +7 -0
  38. package/dist/traits.examples.d.ts.map +1 -0
  39. package/dist/traits.test-d.d.ts +5 -0
  40. package/dist/traits.test-d.d.ts.map +1 -0
  41. package/dist/units/angle.d.ts +43 -0
  42. package/dist/units/angle.d.ts.map +1 -0
  43. package/dist/units/angle.test-d.d.ts +2 -0
  44. package/dist/units/angle.test-d.d.ts.map +1 -0
  45. package/dist/units/color.d.ts +35 -0
  46. package/dist/units/color.d.ts.map +1 -0
  47. package/dist/units/color.test-d.d.ts +2 -0
  48. package/dist/units/color.test-d.d.ts.map +1 -0
  49. package/dist/units/index.cjs.js +2 -0
  50. package/dist/units/index.cjs.js.map +1 -0
  51. package/dist/units/index.d.ts +19 -0
  52. package/dist/units/index.d.ts.map +1 -0
  53. package/dist/units/index.es.js +86 -0
  54. package/dist/units/index.es.js.map +1 -0
  55. package/dist/units/integer.d.ts +12 -0
  56. package/dist/units/integer.d.ts.map +1 -0
  57. package/dist/units/integer.test-d.d.ts +2 -0
  58. package/dist/units/integer.test-d.d.ts.map +1 -0
  59. package/dist/units/length.d.ts +101 -0
  60. package/dist/units/length.d.ts.map +1 -0
  61. package/dist/units/length.test-d.d.ts +2 -0
  62. package/dist/units/length.test-d.d.ts.map +1 -0
  63. package/dist/units/normalized.d.ts +111 -0
  64. package/dist/units/normalized.d.ts.map +1 -0
  65. package/dist/units/normalized.test-d.d.ts +2 -0
  66. package/dist/units/normalized.test-d.d.ts.map +1 -0
  67. package/dist/units/temperature.d.ts +16 -0
  68. package/dist/units/temperature.d.ts.map +1 -0
  69. package/dist/units/temperature.test-d.d.ts +2 -0
  70. package/dist/units/temperature.test-d.d.ts.map +1 -0
  71. package/dist/units/time.d.ts +38 -0
  72. package/dist/units/time.d.ts.map +1 -0
  73. package/dist/units/time.test-d.d.ts +2 -0
  74. package/dist/units/time.test-d.d.ts.map +1 -0
  75. package/dist/units/velocity.d.ts +78 -0
  76. package/dist/units/velocity.d.ts.map +1 -0
  77. package/dist/units/velocity.test-d.d.ts +2 -0
  78. package/dist/units/velocity.test-d.d.ts.map +1 -0
  79. package/package.json +39 -21
  80. package/dist/__tests__/SmartPrimitive.test-d.d.ts.map +0 -1
  81. /package/dist/{__tests__/SmartPrimitive.test-d.d.ts → SmartPrimitive.test-d.d.ts} +0 -0
package/README.md CHANGED
@@ -1,267 +1,302 @@
1
1
  # @penner/smart-primitive
2
2
 
3
- Type-safe branded primitives with zero runtime overhead. Prevent bugs by distinguishing different kinds of numbers, strings, and booleans at compile time.
3
+ Type-safe primitives for TypeScript. Catch unit mix-ups at compile time with zero runtime cost.
4
4
 
5
- ## Why Smart Primitives?
5
+ ## Installation
6
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.
7
+ ```bash
8
+ npm install @penner/smart-primitive
9
+ ```
8
10
 
9
- ```typescript
10
- import { SmartNumber, SmartString } from '@penner/smart-primitive';
11
+ ## Quick Start
11
12
 
12
- type Pixels = SmartNumber<'Pixels'>;
13
- type Milliseconds = SmartNumber<'Milliseconds'>;
14
- type URL = SmartString<'URL'>;
15
- type CSSSelector = SmartString<'CSSSelector'>;
13
+ Import ready-to-use types, annotate your variables and parameters, and let TypeScript catch mistakes:
16
14
 
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';
15
+ ```typescript
16
+ import { Pixels, Milliseconds, Degrees } from '@penner/smart-primitive';
22
17
 
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
- ```
18
+ function animate(
19
+ element: HTMLElement,
20
+ distance: Pixels,
21
+ duration: Milliseconds,
22
+ rotation: Degrees,
23
+ ) {
24
+ // ...
25
+ }
27
26
 
28
- ## Features
27
+ const dist: Pixels = 100;
28
+ const time: Milliseconds = 500;
29
+ const angle: Degrees = 90;
29
30
 
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
31
+ animate(el, dist, time, angle); // correct
32
+ animate(el, time, dist, angle); // Error! Can't use Milliseconds where Pixels expected
33
+ ```
36
34
 
37
- ## Installation
35
+ Plain values are always assignable — no casting or wrapping required:
38
36
 
39
- ```bash
40
- npm install @penner/smart-primitive
37
+ ```typescript
38
+ const width: Pixels = 300; // ✅ plain number works
39
+ const delay: Milliseconds = 1000; // ✅ plain number works
40
+ const opacity: Alpha = 0.8; // ✅ plain number works
41
41
  ```
42
42
 
43
- ## Quick Start
43
+ ### Available Unit Types
44
44
 
45
- ### Smart Numbers
45
+ | Category | Types |
46
+ | ----------- | -------------------------------------------------------------------------------------------------------------------- |
47
+ | Time | `Seconds`, `Milliseconds`, `Minutes`, `Hours` |
48
+ | Length | `Pixels`, `Ems`, `Rems`, `Vw`, `Vh`, `Percent`, `Points`, `Inches`, `Centimeters`, `Millimeters`, `Meters` |
49
+ | Angle | `Degrees`, `Radians`, `Turns` |
50
+ | Normalized | `Normalized` (0–1), `SignedNormalized` (−1–1), `ClampedNormalized`, `Percentage` (0–100), `Alpha`, `Ratio`, `Factor` |
51
+ | Temperature | `Celsius`, `Fahrenheit` |
52
+ | Color | `ColorByte` (0–255), `RGBTuple`, `RGBATuple` |
53
+ | Integer | `Index` |
54
+ | Animation | `NormalizedTime` (clamped 0–1), `NormalizedProgress` (can overshoot), `PercentProgress` |
46
55
 
47
- Perfect for units, measurements, or any numeric domain:
56
+ All types are importable from the package root or from `@penner/smart-primitive/units`.
48
57
 
49
- ```typescript
50
- import { SmartNumber } from '@penner/smart-primitive';
58
+ ## The Trait System
51
59
 
52
- type Pixels = SmartNumber<'Pixels'>;
53
- type Milliseconds = SmartNumber<'Milliseconds'>;
54
- type Degrees = SmartNumber<'Degrees'>;
60
+ These types are built from a small set of composable **traits** — phantom interfaces combined via TypeScript intersection (`&`).
55
61
 
56
- function animate(distance: Pixels, duration: Milliseconds, rotation: Degrees) {
57
- console.log(`Move ${distance}px over ${duration}ms, rotate ${rotation}°`);
58
- }
62
+ ### How Types Are Composed
59
63
 
60
- animate(100, 500, 90); // works
61
- animate(100, 500, 500); // ✅ works (but is it degrees or milliseconds? TypeScript doesn't know yet)
64
+ A simple type like `Pixels` needs just a unit and kind:
62
65
 
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
+ ```typescript
67
+ // Pixels = number with unit 'px' and kind 'length'
68
+ type Pixels = WithUnit<'px'> & WithKind<'length'>;
66
69
  ```
67
70
 
68
- ### Smart Strings
69
-
70
- Distinguish between different kinds of strings:
71
+ A richer type layers on more traits:
71
72
 
72
73
  ```typescript
73
- import { SmartString } from '@penner/smart-primitive';
74
+ // Degrees = number with unit 'deg', range [0, 360), periodic wrapping, kind 'angle'
75
+ type Degrees = WithUnit<'deg'> &
76
+ WithRange<0, 360> &
77
+ WithPeriodic &
78
+ WithKind<'angle'>;
79
+ ```
74
80
 
75
- type URL = SmartString<'URL'>;
76
- type EmailAddress = SmartString<'EmailAddress'>;
77
- type CSSSelector = SmartString<'CSSSelector'>;
81
+ Some types only need a range:
78
82
 
79
- function fetchData(endpoint: URL) {
80
- // implementation
81
- }
83
+ ```typescript
84
+ // Normalized = number in the range [0, 1], unclamped (can overshoot)
85
+ type Normalized = WithRange<0, 1>;
82
86
 
83
- function sendEmail(address: EmailAddress) {
84
- // implementation
85
- }
87
+ // ClampedNormalized = Normalized that's guaranteed within bounds
88
+ type ClampedNormalized = Normalized & WithClamped;
89
+ ```
86
90
 
87
- let api: URL = 'https://api.example.com';
88
- let email: EmailAddress = 'user@example.com';
91
+ ### Core Traits
89
92
 
90
- fetchData(api); // ✅ works
91
- fetchData(email); // Error! EmailAddress is not a URL
92
- ```
93
+ | Trait | Purpose |
94
+ | --------------------- | ---------------------------------------------------------------------------- |
95
+ | `WithUnit<U>` | Labels the measurement unit (`'px'`, `'ms'`, `'deg'`, …) |
96
+ | `WithRange<Min, Max>` | Documents the reference range (informational) |
97
+ | `WithClamped` | Marks the value as constrained to its range |
98
+ | `WithPeriodic` | Marks the value as wrapping at range boundaries |
99
+ | `WithKind<K>` | Groups related types under an archetype (`'length'`, `'time'`, `'angle'`, …) |
100
+ | `WithInteger` | Marks the value as a whole number |
93
101
 
94
- ### Smart Booleans
102
+ ### Defining Custom Types
95
103
 
96
- Even booleans can benefit from type safety:
104
+ Compose traits to define your own domain types:
97
105
 
98
106
  ```typescript
99
- import { SmartBoolean } from '@penner/smart-primitive';
100
-
101
- type IsVisible = SmartBoolean<'IsVisible'>;
102
- type IsEnabled = SmartBoolean<'IsEnabled'>;
107
+ import {
108
+ WithUnit,
109
+ WithRange,
110
+ WithClamped,
111
+ WithKind,
112
+ } from '@penner/smart-primitive';
103
113
 
104
- function toggleVisibility(visible: IsVisible) {
105
- // implementation
106
- }
114
+ // A simple branded unit
115
+ type Frames = WithUnit<'frames'> & WithKind<'time'>;
107
116
 
108
- let visible: IsVisible = true;
109
- let enabled: IsEnabled = true;
117
+ // A bounded value
118
+ type Volume = WithRange<0, 100> & WithUnit<'percent'> & WithClamped;
110
119
 
111
- toggleVisibility(visible); // works
112
- toggleVisibility(enabled); // Error! IsEnabled is not IsVisible
120
+ // An unbounded measurement
121
+ type Force = WithUnit<'N'> & WithKind<'force'>;
113
122
  ```
114
123
 
115
- ## Advanced Usage
124
+ ### Trait Extraction
116
125
 
117
- ### Utility Types
126
+ Query type-level metadata from trait-based types:
118
127
 
119
- #### `Unbrand<T>`
128
+ ```typescript
129
+ import type {
130
+ UnitOf,
131
+ RangeOf,
132
+ MinOf,
133
+ MaxOf,
134
+ IsClamped,
135
+ IsPeriodic,
136
+ KindOf,
137
+ } from '@penner/smart-primitive';
138
+
139
+ type U = UnitOf<Degrees>; // 'deg'
140
+ type R = RangeOf<Degrees>; // readonly [0, 360]
141
+ type K = KindOf<Pixels>; // 'length'
142
+ type C = IsClamped<Alpha>; // false
143
+ type P = IsPeriodic<Degrees>; // true
144
+ ```
145
+
146
+ ### Trait Modification
120
147
 
121
- Remove branding from types, converting them back to primitives:
148
+ Transform types by adding, removing, or changing traits:
122
149
 
123
150
  ```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
- };
151
+ import type {
152
+ WithoutClamped,
153
+ ChangeUnits,
154
+ ChangeRange,
155
+ } from '@penner/smart-primitive';
156
+
157
+ type Unclamped = WithoutClamped<ClampedNormalized>; // remove clamping
158
+ type InInches = ChangeUnits<Pixels, 'in'>; // swap units
159
+ type WiderRange = ChangeRange<Normalized, -1, 2>; // change range
140
160
  ```
141
161
 
142
- #### `BaseOf<T>`
162
+ ### Runtime Utilities
143
163
 
144
- Extract the base primitive type:
164
+ The `clamp` and `wrap` functions operate on trait-based types and refine the output type accordingly:
145
165
 
146
166
  ```typescript
147
- import { BaseOf } from '@penner/smart-primitive';
167
+ import { clamp, wrap } from '@penner/smart-primitive';
148
168
 
149
- type PixelsBase = BaseOf<Pixels>; // number
150
- type URLBase = BaseOf<URL>; // string
151
- type IsVisibleBase = BaseOf<IsVisible>; // boolean
169
+ const v = clamp(1.5, 0, 1); // value: 1, type: WithRange<0, 1> & WithClamped
170
+ const w = wrap(450, 0, 360); // value: 90, type: WithRange<0, 360> & WithPeriodic
152
171
  ```
153
172
 
154
- #### `UnbrandFn<F>`
173
+ ## Conversions
155
174
 
156
- Unbrand function parameters:
175
+ Each unit module ships pre-built converter functions:
157
176
 
158
177
  ```typescript
159
- import { UnbrandFn } from '@penner/smart-primitive';
178
+ import { degreesToRadians, radiansToDegrees } from '@penner/smart-primitive';
179
+ import {
180
+ secondsToMilliseconds,
181
+ celsiusToFahrenheit,
182
+ } from '@penner/smart-primitive';
183
+ import {
184
+ normalizedToPercentage,
185
+ metersToCentimeters,
186
+ } from '@penner/smart-primitive';
187
+
188
+ const rad: Radians = degreesToRadians(90); // π/2
189
+ const ms: Milliseconds = secondsToMilliseconds(2); // 2000
190
+ const pct: Percentage = normalizedToPercentage(0.75); // 75
191
+ ```
160
192
 
161
- function animate(distance: Pixels, duration: Milliseconds): void {
162
- // implementation
163
- }
193
+ For custom conversions, use the factory functions:
164
194
 
165
- type PlainAnimate = UnbrandFn<typeof animate>;
166
- // Result: (distance: number, duration: number) => void
195
+ ```typescript
196
+ import {
197
+ createConverter,
198
+ createBiConverter,
199
+ createLinearConverter,
200
+ } from '@penner/smart-primitive';
201
+
202
+ // One-way converter
203
+ const framesToSeconds = createConverter<Frames, Seconds>(f => f / 60);
204
+
205
+ // Bidirectional converter
206
+ const pixelsRems = createBiConverter<Pixels, Rems>(
207
+ px => px / 16,
208
+ rem => rem * 16,
209
+ );
210
+ pixelsRems.to(32); // 2 Rems
211
+ pixelsRems.from(1.5); // 24 Pixels
212
+
213
+ // Linear ratio converter
214
+ const metersCm = createLinearConverter<Meters, Centimeters>(100);
167
215
  ```
168
216
 
169
- ### Feature Flag: Toggle Type Safety
217
+ Advanced patterns like `chain` (compose converters) and `ConverterRegistry` (runtime lookup) are also available.
170
218
 
171
- You can disable all smart type checking with a single flag. This is useful for:
219
+ ## String Types
172
220
 
173
- - Performance testing
174
- - Debugging type issues
175
- - Gradual migration
176
- - Bundle size optimization
221
+ The library includes semantic string types via `@penner/smart-primitive/strings`, with template literal validation where possible:
177
222
 
178
223
  ```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)
224
+ import type {
225
+ CSSLength,
226
+ CSSCustomProperty,
227
+ UUID,
228
+ EmailAddress,
229
+ } from '@penner/smart-primitive/strings';
230
+
231
+ const width: CSSLength = '16px'; // template: `${number}${CSSLengthUnit}`
232
+ const prop: CSSCustomProperty = '--main-color'; // template: `--${string}`
233
+ const id: UUID = '550e8400-e29b-41d4-a716-446655440000';
234
+ const email: EmailAddress = 'user@example.com';
191
235
  ```
192
236
 
193
- ## How It Works
237
+ See the [strings module](./src/strings/) for the full list of CSS, web, and data format types.
194
238
 
195
- Smart primitives use TypeScript's brand pattern (also called phantom types or nominal typing). The implementation is remarkably simple:
239
+ ## SmartPrimitive & SmartNumber
196
240
 
197
- ```typescript
198
- export type SmartPrimitive<
199
- Base extends string | number | boolean | bigint | symbol,
200
- BrandName extends string,
201
- > = Base & { readonly __brand?: BrandName };
202
- ```
241
+ For quick, ad-hoc branded types that don't need trait composition, `SmartNumber` and `SmartString` are still available:
203
242
 
204
- The `__brand` property is:
243
+ ```typescript
244
+ import { SmartNumber, SmartString } from '@penner/smart-primitive';
205
245
 
206
- - **Optional** - so plain primitives are assignable
207
- - **Readonly** - prevents accidental modification
208
- - **Never actually exists at runtime** - TypeScript-only, zero overhead
246
+ type Score = SmartNumber<'Score'>;
247
+ type SessionToken = SmartString<'SessionToken'>;
209
248
 
210
- ## TypeScript Compatibility
249
+ const points: Score = 42;
250
+ const token: SessionToken = 'abc123';
251
+ ```
211
252
 
212
- Requires TypeScript 4.5 or higher.
253
+ > **Note:** All pre-built unit types (`Pixels`, `Degrees`, `Milliseconds`, etc.) are now defined using the trait system rather than `SmartNumber`. Trait-based types carry richer metadata (units, ranges, kinds) and support extraction utilities like `UnitOf` and `RangeOf`. For a discussion of the tradeoffs between the two approaches, see [SmartPrimitive vs Traits analysis](../../docs/research/smart-primitive-vs-traits.md).
213
254
 
214
- ## Examples
255
+ ## Feature Flag: Toggle Type Safety
215
256
 
216
- ### Complex Object Structures
257
+ Disable all smart typing across your project via module augmentation:
217
258
 
218
259
  ```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
- };
260
+ // In a .d.ts file in your project (e.g., smart-primitive.d.ts):
261
+ declare module '@penner/smart-primitive' {
262
+ interface SmartPrimitiveConfig {
263
+ usePlainPrimitives: true;
264
+ }
265
+ }
236
266
  ```
237
267
 
238
- ### Function Safety
268
+ When enabled, all smart types collapse to plain primitives (`number`, `string`, etc.) and all cross-type assignments are allowed. Useful for debugging type issues or gradual migration.
239
269
 
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
- }
270
+ ## Utility Types
271
+
272
+ ### `Unbrand<T>`
273
+
274
+ Strip branding from types, recursing into object structures:
248
275
 
249
- let dist: Pixels = 100;
250
- let time: Milliseconds = 500;
276
+ ```typescript
277
+ import type { Unbrand } from '@penner/smart-primitive';
251
278
 
252
- moveElement(element, dist, time); // correct
253
- moveElement(element, time, dist); // ❌ Error! Parameters swapped
279
+ type Config = { width: Pixels; duration: Milliseconds };
280
+ type Plain = Unbrand<Config>;
281
+ // { width: number; duration: number }
254
282
  ```
255
283
 
256
- ## License
284
+ ### `UnbrandFn<F>`
257
285
 
258
- MIT
286
+ Strip branding from function parameter types:
259
287
 
260
- ## Related Packages
288
+ ```typescript
289
+ import type { UnbrandFn } from '@penner/smart-primitive';
261
290
 
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
291
+ declare function move(distance: Pixels, duration: Milliseconds): void;
292
+ type PlainMove = UnbrandFn<typeof move>;
293
+ // (distance: number, duration: number) => void
294
+ ```
295
+
296
+ ## TypeScript Compatibility
264
297
 
265
- ## Contributing
298
+ Requires TypeScript 4.5 or higher.
266
299
 
267
- Contributions welcome! Please read the [contributing guidelines](../../CONTRIBUTING.md) first.
300
+ ## License
301
+
302
+ MIT