Skip to content

Commit 32a7291

Browse files
committed
feat(types): add LooseToStrict and Brand utility types with JSDocs
- Introduced `LooseToStrict<T>` to filter out broad string types (e.g., `string`) and retain only strict string literals or narrower unions. - Added `Brand<T, Brand extends string>` to create nominally typed aliases for primitive types using unique symbols. - Provided detailed JSDocs with examples for both types to improve developer understanding and usage. - Exported both types from the package entrypoint for public use.
1 parent 19bf23b commit 32a7291

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

src/docs/docs.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,72 @@ export type DeepReadonly<T> = {
105105
export type Prettify<T> = {
106106
[K in keyof T]: T[K];
107107
} & {};
108+
109+
declare const brand: unique symbol;
110+
/**
111+
* Creates a *branded* version of a base type, allowing nominal typing in TypeScript.
112+
*
113+
* By default, TypeScript uses *structural typing*, which means types with the same shape are considered interchangeable.
114+
* `Brand<T, B>` allows you to distinguish between otherwise identical types by attaching a unique hidden "brand" to the type.
115+
*
116+
* This is useful for creating safer APIs where different domains use the same primitive types (like `string` or `number`)
117+
* but should not be mixed up unintentionally.
118+
*
119+
* @template T - The base type to brand (e.g. `string`, `number`, etc).
120+
* @template Brand - A string literal representing the brand (e.g. `'UserId'`, `'FilePath'`).
121+
*
122+
* @example
123+
* ```ts
124+
* type UserId = Brand<string, 'UserId'>;
125+
* type PostId = Brand<string, 'PostId'>;
126+
*
127+
* function getUser(id: UserId) { ... }
128+
*
129+
* const userId = 'abc123' as UserId;
130+
* const postId = 'xyz789' as PostId;
131+
*
132+
* getUser(userId); // ✅ OK
133+
* getUser(postId); // ❌ Type error: PostId is not assignable to UserId
134+
* ```
135+
*
136+
* @example
137+
* ```ts
138+
* // Branded number
139+
* type Milliseconds = Brand<number, 'Milliseconds'>;
140+
* type Seconds = Brand<number, 'Seconds'>;
141+
*
142+
* const wait = (ms: Milliseconds) => { ... };
143+
* wait(5000 as Milliseconds); // ✅
144+
* wait(5 as Seconds); // ❌ Compile error
145+
* ```
146+
*
147+
* @note This has no runtime effect. The brand is erased in the compiled JavaScript.
148+
* @since v1.0.7
149+
*/
150+
export type Brand<T, Brand extends string> = T & { [brand]: Brand };
151+
152+
/**
153+
* Filters out "loose" string types like the built-in `string` type,
154+
* leaving only "strict" string types such as string literals or narrower unions.
155+
*
156+
* This is useful when you want to **exclude broad string types** and keep
157+
* only more specific, literal string types in a union.
158+
*
159+
* The type distributes over unions, testing each member individually.
160+
*
161+
* @example
162+
* type Test1 = LooseToStrict<string>;
163+
* // Result: never (excluded because `string` extends `string`)
164+
*
165+
* @example
166+
* type Test2 = LooseToStrict<'hello' | string>;
167+
* // Result: 'hello' (excludes `string`, keeps the literal)
168+
*
169+
* @example
170+
* type Test3 = LooseToStrict<'foo' | 'bar'>;
171+
* // Result: 'foo' | 'bar' (kept because `string` does NOT extend these literals)
172+
*
173+
* @template T - The type or union of types to filter.
174+
* @since v1.0.7
175+
*/
176+
export type LooseToStrict<T> = T extends any ? (string extends T ? never : T) : never;

src/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ export type {
99
NonEmptyArray,
1010
AccessPermissions, PathAccessPermissions, AccessOptions,
1111
DiscoverHostsOptions,
12-
DeepReadonly,
13-
Objects,
14-
Prettify,
12+
Objects, DeepReadonly, Prettify, Brand, LooseToStrict,
1513
StringPaddingOptions,
1614
Serializable,
1715
RandomOptions,

0 commit comments

Comments
 (0)