@penner/smart-primitive
v0.0.1
Published
Type-safe branded primitives with zero runtime overhead - prevent bugs by distinguishing different kinds of numbers, strings, and booleans
Maintainers
Readme
@penner/smart-primitive
Type-safe branded primitives with zero runtime overhead. Prevent bugs by distinguishing different kinds of numbers, strings, and booleans at compile time.
Why Smart Primitives?
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.
import { SmartNumber, SmartString } from '@penner/smart-primitive';
type Pixels = SmartNumber<'Pixels'>;
type Milliseconds = SmartNumber<'Milliseconds'>;
type URL = SmartString<'URL'>;
type CSSSelector = SmartString<'CSSSelector'>;
// ✅ Works perfectly
let width: Pixels = 300;
let delay: Milliseconds = 500;
let link: URL = 'https://example.com';
let selector: CSSSelector = '.button';
// ❌ TypeScript catches the mistake!
let oops: Pixels = delay; // Error: Type 'Milliseconds' is not assignable to type 'Pixels'
let wrong: URL = selector; // Error: Type 'CSSSelector' is not assignable to type 'URL'Features
- ✅ Zero runtime overhead - Pure TypeScript, no JavaScript generated
- ✅ Works with plain values - No wrapping or conversion needed
- ✅ Prevents cross-domain mixing - TypeScript stops you from mixing incompatible types
- ✅ Toggleable type safety - Turn off all smart typing with one flag
- ✅ Utility functions -
Unbrand,BaseOf,UnbrandFnfor working with branded types - ✅ Clean tooltips - TypeScript shows clean type names, not implementation details
Installation
npm install @penner/smart-primitiveQuick Start
Smart Numbers
Perfect for units, measurements, or any numeric domain:
import { SmartNumber } from '@penner/smart-primitive';
type Pixels = SmartNumber<'Pixels'>;
type Milliseconds = SmartNumber<'Milliseconds'>;
type Degrees = SmartNumber<'Degrees'>;
function animate(distance: Pixels, duration: Milliseconds, rotation: Degrees) {
console.log(`Move ${distance}px over ${duration}ms, rotate ${rotation}°`);
}
animate(100, 500, 90); // ✅ works
animate(100, 500, 500); // ✅ works (but is it degrees or milliseconds? TypeScript doesn't know yet)
// But if you assign first:
let delay: Milliseconds = 500;
animate(100, delay, delay); // ❌ Error! Can't use Milliseconds where Degrees expectedSmart Strings
Distinguish between different kinds of strings:
import { SmartString } from '@penner/smart-primitive';
type URL = SmartString<'URL'>;
type EmailAddress = SmartString<'EmailAddress'>;
type CSSSelector = SmartString<'CSSSelector'>;
function fetchData(endpoint: URL) {
// implementation
}
function sendEmail(address: EmailAddress) {
// implementation
}
let api: URL = 'https://api.example.com';
let email: EmailAddress = '[email protected]';
fetchData(api); // ✅ works
fetchData(email); // ❌ Error! EmailAddress is not a URLSmart Booleans
Even booleans can benefit from type safety:
import { SmartBoolean } from '@penner/smart-primitive';
type IsVisible = SmartBoolean<'IsVisible'>;
type IsEnabled = SmartBoolean<'IsEnabled'>;
function toggleVisibility(visible: IsVisible) {
// implementation
}
let visible: IsVisible = true;
let enabled: IsEnabled = true;
toggleVisibility(visible); // ✅ works
toggleVisibility(enabled); // ❌ Error! IsEnabled is not IsVisibleAdvanced Usage
Utility Types
Unbrand<T>
Remove branding from types, converting them back to primitives:
import { Unbrand } from '@penner/smart-primitive';
type Config = {
width: Pixels;
duration: Milliseconds;
url: URL;
};
type PlainConfig = Unbrand<Config>;
// Result: { width: number; duration: number; url: string; }
const config: PlainConfig = {
width: 100,
duration: 500,
url: 'https://example.com',
};BaseOf<T>
Extract the base primitive type:
import { BaseOf } from '@penner/smart-primitive';
type PixelsBase = BaseOf<Pixels>; // number
type URLBase = BaseOf<URL>; // string
type IsVisibleBase = BaseOf<IsVisible>; // booleanUnbrandFn<F>
Unbrand function parameters:
import { UnbrandFn } from '@penner/smart-primitive';
function animate(distance: Pixels, duration: Milliseconds): void {
// implementation
}
type PlainAnimate = UnbrandFn<typeof animate>;
// Result: (distance: number, duration: number) => voidFeature Flag: Toggle Type Safety
You can disable all smart type checking with a single flag. This is useful for:
- Performance testing
- Debugging type issues
- Gradual migration
- Bundle size optimization
// In your SmartPrimitive.ts file
export const USE_PLAIN_PRIMITIVES = true as const; // 👈 Change to true
// Now ALL smart types become plain primitives
type Pixels = SmartNumber<'Pixels'>; // becomes: number
type URL = SmartString<'URL'>; // becomes: string
type IsVisible = SmartBoolean<'IsVisible'>; // becomes: boolean
// All cross-brand assignments are now allowed
let width: Pixels = 100;
let delay: Milliseconds = 200;
width = delay; // ✅ Now allowed! (when flag is true)How It Works
Smart primitives use TypeScript's brand pattern (also called phantom types or nominal typing). The implementation is remarkably simple:
export type SmartPrimitive<
Base extends string | number | boolean | bigint | symbol,
BrandName extends string,
> = Base & { readonly __brand?: BrandName };The __brand property is:
- Optional - so plain primitives are assignable
- Readonly - prevents accidental modification
- Never actually exists at runtime - TypeScript-only, zero overhead
TypeScript Compatibility
Requires TypeScript 4.5 or higher.
Examples
Complex Object Structures
type AnimationConfig = {
timing: {
duration: Milliseconds;
delay: Milliseconds;
};
position: {
start: Pixels;
end: Pixels;
};
rotation: Degrees;
};
const config: AnimationConfig = {
timing: { duration: 1000, delay: 200 },
position: { start: 0, end: 500 },
rotation: 180,
};Function Safety
function moveElement(
element: HTMLElement,
distance: Pixels,
duration: Milliseconds,
): void {
// TypeScript ensures you can't accidentally swap parameters
}
let dist: Pixels = 100;
let time: Milliseconds = 500;
moveElement(element, dist, time); // ✅ correct
moveElement(element, time, dist); // ❌ Error! Parameters swappedLicense
MIT
Related Packages
@penner/easing- Modern Penner easing functions@penner/responsive-easing- Responsive motion design system
Contributing
Contributions welcome! Please read the contributing guidelines first.
