@outfitter/types 0.1.0-rc.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,13 @@
1
+ # @outfitter/types
2
+
3
+ Branded types, type guards, and type utilities for Outfitter.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @outfitter/types
9
+ ```
10
+
11
+ ## Status
12
+
13
+ This package is in early development. API may change.
@@ -0,0 +1,129 @@
1
+ import { ValidationError } from "@outfitter/contracts";
2
+ import { Result } from "better-result";
3
+ /**
4
+ * Creates a branded type by adding a unique brand to a base type.
5
+ * This enables nominal typing for primitive types.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * type UserId = Branded<string, "UserId">;
10
+ * type PostId = Branded<string, "PostId">;
11
+ *
12
+ * // These are now incompatible at compile time
13
+ * const userId: UserId = brand<string, "UserId">("user_123");
14
+ * const postId: PostId = brand<string, "PostId">("post_456");
15
+ * ```
16
+ */
17
+ type Branded<
18
+ T,
19
+ Brand extends string
20
+ > = T & {
21
+ readonly __brand: Brand;
22
+ };
23
+ /**
24
+ * Brands a value with a nominal type marker.
25
+ *
26
+ * @param value - The value to brand
27
+ * @returns The branded value
28
+ * @throws Error - Not implemented yet
29
+ */
30
+ declare function brand<
31
+ T,
32
+ Brand extends string
33
+ >(value: T): Branded<T, Brand>;
34
+ /**
35
+ * Removes the brand from a branded type, returning the underlying type.
36
+ *
37
+ * @param value - The branded value
38
+ * @returns The unbranded value
39
+ * @throws Error - Not implemented yet
40
+ */
41
+ declare function unbrand<
42
+ T,
43
+ Brand extends string
44
+ >(value: Branded<T, Brand>): T;
45
+ /**
46
+ * Type helper to extract the underlying type from a branded type.
47
+ */
48
+ type Unbrand<T> = T extends Branded<infer U, string> ? U : T;
49
+ /**
50
+ * Type helper to extract the brand string from a branded type.
51
+ */
52
+ type BrandOf<T> = T extends Branded<unknown, infer B> ? B : never;
53
+ /**
54
+ * A positive integer (value > 0, must be a finite integer).
55
+ */
56
+ type PositiveInt = Branded<number, "PositiveInt">;
57
+ /**
58
+ * A non-empty string (trimmed length > 0).
59
+ */
60
+ type NonEmptyString = Branded<string, "NonEmptyString">;
61
+ /**
62
+ * An email address (basic format validation: contains @ and . in domain).
63
+ */
64
+ type Email = Branded<string, "Email">;
65
+ /**
66
+ * A UUID string (UUID v4 format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
67
+ */
68
+ type UUID = Branded<string, "UUID">;
69
+ /**
70
+ * Creates a PositiveInt from a number, validating that it is a positive integer.
71
+ *
72
+ * @param value - The number to validate
73
+ * @returns Result containing PositiveInt on success, ValidationError on failure
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const result = positiveInt(42);
78
+ * if (Result.isOk(result)) {
79
+ * console.log(result.value); // 42 as PositiveInt
80
+ * }
81
+ * ```
82
+ */
83
+ declare function positiveInt(value: number): Result<PositiveInt, InstanceType<typeof ValidationError>>;
84
+ /**
85
+ * Creates a NonEmptyString from a string, validating that it has content after trimming.
86
+ *
87
+ * @param value - The string to validate
88
+ * @returns Result containing NonEmptyString on success, ValidationError on failure
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const result = nonEmptyString("hello");
93
+ * if (Result.isOk(result)) {
94
+ * console.log(result.value); // "hello" as NonEmptyString
95
+ * }
96
+ * ```
97
+ */
98
+ declare function nonEmptyString(value: string): Result<NonEmptyString, InstanceType<typeof ValidationError>>;
99
+ /**
100
+ * Creates an Email from a string, validating basic email format.
101
+ *
102
+ * @param value - The string to validate
103
+ * @returns Result containing Email on success, ValidationError on failure
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const result = email("user@example.com");
108
+ * if (Result.isOk(result)) {
109
+ * console.log(result.value); // "user@example.com" as Email
110
+ * }
111
+ * ```
112
+ */
113
+ declare function email(value: string): Result<Email, InstanceType<typeof ValidationError>>;
114
+ /**
115
+ * Creates a UUID from a string, validating UUID v4 format.
116
+ *
117
+ * @param value - The string to validate
118
+ * @returns Result containing UUID on success, ValidationError on failure
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const result = uuid("550e8400-e29b-41d4-a716-446655440000");
123
+ * if (Result.isOk(result)) {
124
+ * console.log(result.value); // UUID branded string
125
+ * }
126
+ * ```
127
+ */
128
+ declare function uuid(value: string): Result<UUID, InstanceType<typeof ValidationError>>;
129
+ export { uuid, unbrand, positiveInt, nonEmptyString, email, brand, Unbrand, UUID, PositiveInt, NonEmptyString, Email, Branded, BrandOf };
@@ -0,0 +1,68 @@
1
+ // @bun
2
+ // packages/types/src/branded.ts
3
+ import { ValidationError } from "@outfitter/contracts";
4
+ import { Result } from "better-result";
5
+ function brand(value) {
6
+ return value;
7
+ }
8
+ function unbrand(value) {
9
+ return value;
10
+ }
11
+ function positiveInt(value) {
12
+ if (!Number.isFinite(value)) {
13
+ return Result.err(new ValidationError({
14
+ message: "Value must be a finite number",
15
+ field: "value"
16
+ }));
17
+ }
18
+ if (!Number.isInteger(value)) {
19
+ return Result.err(new ValidationError({
20
+ message: "Value must be an integer",
21
+ field: "value"
22
+ }));
23
+ }
24
+ if (value <= 0) {
25
+ return Result.err(new ValidationError({
26
+ message: "Value must be greater than 0",
27
+ field: "value"
28
+ }));
29
+ }
30
+ return Result.ok(brand(value));
31
+ }
32
+ function nonEmptyString(value) {
33
+ if (value.trim().length === 0) {
34
+ return Result.err(new ValidationError({
35
+ message: "String must not be empty or whitespace-only",
36
+ field: "value"
37
+ }));
38
+ }
39
+ return Result.ok(brand(value));
40
+ }
41
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
42
+ function email(value) {
43
+ if (!EMAIL_REGEX.test(value)) {
44
+ return Result.err(new ValidationError({
45
+ message: "Invalid email format",
46
+ field: "value"
47
+ }));
48
+ }
49
+ return Result.ok(brand(value));
50
+ }
51
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
52
+ function uuid(value) {
53
+ if (!UUID_REGEX.test(value)) {
54
+ return Result.err(new ValidationError({
55
+ message: "Invalid UUID v4 format",
56
+ field: "value"
57
+ }));
58
+ }
59
+ return Result.ok(brand(value));
60
+ }
61
+ export {
62
+ uuid,
63
+ unbrand,
64
+ positiveInt,
65
+ nonEmptyString,
66
+ email,
67
+ brand
68
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Collection type utilities.
3
+ *
4
+ * @module collections
5
+ */
6
+ /**
7
+ * A non-empty array type that guarantees at least one element.
8
+ */
9
+ type NonEmptyArray<T> = [T, ...T[]];
10
+ /**
11
+ * Type guard for checking if an array is non-empty.
12
+ *
13
+ * @param array - The array to check
14
+ * @returns True if the array has at least one element
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const items: string[] = getItems();
19
+ * if (isNonEmptyArray(items)) {
20
+ * const first = items[0]; // Type is string, not string | undefined
21
+ * }
22
+ * ```
23
+ */
24
+ declare function isNonEmptyArray<T>(array: T[]): array is NonEmptyArray<T>;
25
+ /**
26
+ * Creates a non-empty array, throwing if the input is empty.
27
+ *
28
+ * @param array - The array to convert
29
+ * @returns A non-empty array
30
+ * @throws Error - If the array is empty
31
+ * @throws Error - Not implemented yet
32
+ */
33
+ declare function toNonEmptyArray<T>(array: T[]): NonEmptyArray<T>;
34
+ /**
35
+ * Gets the first element of a non-empty array.
36
+ *
37
+ * @param array - A non-empty array
38
+ * @returns The first element (guaranteed to exist)
39
+ */
40
+ declare function first<T>(array: NonEmptyArray<T>): T;
41
+ /**
42
+ * Gets the last element of a non-empty array.
43
+ *
44
+ * @param array - A non-empty array
45
+ * @returns The last element (guaranteed to exist)
46
+ */
47
+ declare function last<T>(array: NonEmptyArray<T>): T;
48
+ /**
49
+ * Groups array elements by a key function.
50
+ *
51
+ * @param items - The items to group
52
+ * @param keyFn - Function to extract the grouping key
53
+ * @returns A map of keys to arrays of items
54
+ * @throws Error - Not implemented yet
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const users = [{ name: "Alice", role: "admin" }, { name: "Bob", role: "user" }];
59
+ * const byRole = groupBy(users, (u) => u.role);
60
+ * // Map { "admin" => [{ name: "Alice", ... }], "user" => [{ name: "Bob", ... }] }
61
+ * ```
62
+ */
63
+ declare function groupBy<
64
+ T,
65
+ K extends string | number | symbol
66
+ >(items: T[], keyFn: (item: T) => K): Map<K, NonEmptyArray<T>>;
67
+ export { toNonEmptyArray, last, isNonEmptyArray, groupBy, first, NonEmptyArray };
@@ -0,0 +1,37 @@
1
+ // @bun
2
+ // packages/types/src/collections.ts
3
+ function isNonEmptyArray(array) {
4
+ return array.length > 0;
5
+ }
6
+ function toNonEmptyArray(array) {
7
+ if (array.length === 0) {
8
+ throw new Error("Array is empty");
9
+ }
10
+ return array;
11
+ }
12
+ function first(array) {
13
+ return array[0];
14
+ }
15
+ function last(array) {
16
+ return array.at(-1);
17
+ }
18
+ function groupBy(items, keyFn) {
19
+ const result = new Map;
20
+ for (const item of items) {
21
+ const key = keyFn(item);
22
+ const existing = result.get(key);
23
+ if (existing) {
24
+ existing.push(item);
25
+ } else {
26
+ result.set(key, [item]);
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ export {
32
+ toNonEmptyArray,
33
+ last,
34
+ isNonEmptyArray,
35
+ groupBy,
36
+ first
37
+ };
package/dist/deep.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Deep path type utilities for type-safe dot-notation access.
3
+ *
4
+ * These utilities enable type-safe access to nested object properties using
5
+ * dot-notation string paths, commonly used in configuration systems and
6
+ * state management.
7
+ *
8
+ * @module deep
9
+ */
10
+ /**
11
+ * Recursively extracts all valid dot-notation paths from an object type.
12
+ *
13
+ * This type generates a union of all possible paths through an object,
14
+ * including intermediate object paths and final leaf paths.
15
+ *
16
+ * Arrays are treated as leaf nodes - the type does not recurse into array
17
+ * indices (e.g., "items.0" is not generated for `{ items: string[] }`).
18
+ *
19
+ * @typeParam T - The object type to extract paths from
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * type Config = { database: { host: string; port: number }; debug: boolean };
24
+ * type Keys = DeepKeys<Config>;
25
+ * // "database" | "database.host" | "database.port" | "debug"
26
+ * ```
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * // Arrays are leaf nodes
31
+ * type WithArray = { items: string[]; nested: { list: number[] } };
32
+ * type Keys = DeepKeys<WithArray>;
33
+ * // "items" | "nested" | "nested.list"
34
+ * ```
35
+ */
36
+ type DeepKeys<T> = T extends object ? T extends readonly unknown[] ? never : { [K in keyof T & string] : NonNullable<T[K]> extends object ? NonNullable<T[K]> extends readonly unknown[] ? K : K | `${K}.${DeepKeys<NonNullable<T[K]>>}` : K }[keyof T & string] : never;
37
+ /**
38
+ * Gets the type at a specific dot-notation path.
39
+ *
40
+ * Given an object type and a path string, this type resolves to the
41
+ * type of the value at that path. Returns `never` for invalid paths.
42
+ *
43
+ * @typeParam T - The object type to access
44
+ * @typeParam P - The dot-notation path string
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * type Config = { database: { host: string; port: number } };
49
+ * type Host = DeepGet<Config, "database.host">; // string
50
+ * type Port = DeepGet<Config, "database.port">; // number
51
+ * type DB = DeepGet<Config, "database">; // { host: string; port: number }
52
+ * ```
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * type Config = { database: { host: string } };
57
+ * type Invalid = DeepGet<Config, "database.invalid">; // never
58
+ * ```
59
+ */
60
+ type DeepGet<
61
+ T,
62
+ P extends string
63
+ > = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? DeepGet<T[K], Rest> : never : P extends keyof T ? T[P] : never;
64
+ /**
65
+ * Creates a new type with the value at path P replaced with V.
66
+ *
67
+ * This type is useful for typing immutable update functions that modify
68
+ * values at specific paths while preserving the rest of the object structure.
69
+ *
70
+ * Returns `never` if the path is invalid.
71
+ *
72
+ * @typeParam T - The original object type
73
+ * @typeParam P - The dot-notation path to the value to replace
74
+ * @typeParam V - The new type for the value at the path
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * type Config = { database: { host: string; port: number } };
79
+ *
80
+ * // Replace port type from number to string
81
+ * type Updated = DeepSet<Config, "database.port", string>;
82
+ * // { database: { host: string; port: string } }
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * type State = { user: { name: string; age: number } };
88
+ *
89
+ * // Replace entire nested object
90
+ * type Updated = DeepSet<State, "user", { id: number }>;
91
+ * // { user: { id: number } }
92
+ * ```
93
+ */
94
+ type DeepSet<
95
+ T,
96
+ P extends string,
97
+ V
98
+ > = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? { [Key in keyof T] : Key extends K ? DeepSet<T[Key], Rest, V> : T[Key] } : never : P extends keyof T ? { [Key in keyof T] : Key extends P ? V : T[Key] } : never;
99
+ export { DeepSet, DeepKeys, DeepGet };
package/dist/deep.js ADDED
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Type guard utilities for runtime type checking.
3
+ *
4
+ * @module guards
5
+ */
6
+ /**
7
+ * Type guard for checking if a value is defined (not null or undefined).
8
+ *
9
+ * @param value - The value to check
10
+ * @returns True if the value is not null or undefined
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const items = [1, null, 2, undefined, 3];
15
+ * const defined = items.filter(isDefined); // [1, 2, 3]
16
+ * ```
17
+ */
18
+ declare function isDefined<T>(value: T | null | undefined): value is T;
19
+ /**
20
+ * Type guard for checking if a value is a non-empty string.
21
+ *
22
+ * @param value - The value to check
23
+ * @returns True if the value is a string with length > 0
24
+ */
25
+ declare function isNonEmptyString(value: unknown): value is string;
26
+ /**
27
+ * Type guard for checking if a value is a plain object.
28
+ *
29
+ * @param value - The value to check
30
+ * @returns True if the value is a plain object (not null, array, or other object types)
31
+ */
32
+ declare function isPlainObject(value: unknown): value is Record<string, unknown>;
33
+ /**
34
+ * Type guard for checking if a value has a specific property.
35
+ *
36
+ * @param value - The value to check
37
+ * @param key - The property key to look for
38
+ * @returns True if the value is an object with the specified property
39
+ */
40
+ declare function hasProperty<K extends string>(value: unknown, key: K): value is Record<K, unknown>;
41
+ /**
42
+ * Creates a type guard function for a specific type using a predicate.
43
+ *
44
+ * @param predicate - A function that returns true if the value matches the type
45
+ * @returns A type guard function
46
+ * @throws Error - Not implemented yet
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * interface User { name: string; age: number; }
51
+ * const isUser = createGuard<User>(
52
+ * (v) => hasProperty(v, "name") && hasProperty(v, "age")
53
+ * );
54
+ * ```
55
+ */
56
+ declare function createGuard<T>(predicate: (value: unknown) => boolean): (value: unknown) => value is T;
57
+ /**
58
+ * Asserts that a value matches a type guard, throwing if it doesn't.
59
+ *
60
+ * @param value - The value to assert
61
+ * @param guard - The type guard to use
62
+ * @param message - Optional error message
63
+ * @throws Error - If the value doesn't match the guard
64
+ * @throws Error - Not implemented yet
65
+ */
66
+ declare function assertType<T>(value: unknown, guard: (value: unknown) => value is T, message?: string): asserts value is T;
67
+ export { isPlainObject, isNonEmptyString, isDefined, hasProperty, createGuard, assertType };
package/dist/guards.js ADDED
@@ -0,0 +1,30 @@
1
+ // @bun
2
+ // packages/types/src/guards.ts
3
+ function isDefined(value) {
4
+ return value !== null && value !== undefined;
5
+ }
6
+ function isNonEmptyString(value) {
7
+ return typeof value === "string" && value.length > 0;
8
+ }
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
11
+ }
12
+ function hasProperty(value, key) {
13
+ return isPlainObject(value) && key in value;
14
+ }
15
+ function createGuard(predicate) {
16
+ return (value) => predicate(value);
17
+ }
18
+ function assertType(value, guard, message) {
19
+ if (!guard(value)) {
20
+ throw new Error(message ?? "Type assertion failed");
21
+ }
22
+ }
23
+ export {
24
+ isPlainObject,
25
+ isNonEmptyString,
26
+ isDefined,
27
+ hasProperty,
28
+ createGuard,
29
+ assertType
30
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Generates a deterministic short ID from input using Bun.hash().
3
+ * Unlike shortId() which generates random IDs, hashId() always produces
4
+ * the same output for the same input.
5
+ *
6
+ * @param input - String to hash
7
+ * @param length - Output length (default: 5)
8
+ * @returns URL-safe alphanumeric hash (base62)
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * hashId("user-123") // "a7b2c" (always same for same input)
13
+ * hashId("user-123", 8) // "a7b2c3d1"
14
+ * ```
15
+ */
16
+ declare function hashId(input: string, length?: number): string;
17
+ export { hashId };
@@ -0,0 +1,25 @@
1
+ // @bun
2
+ // packages/types/src/hash-id.ts
3
+ var BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
4
+ function toBase62(num) {
5
+ if (num === 0n)
6
+ return "0";
7
+ const base = BigInt(BASE62.length);
8
+ let result = "";
9
+ let value = num;
10
+ while (value > 0n) {
11
+ const remainder = Number(value % base);
12
+ result = BASE62[remainder] + result;
13
+ value /= base;
14
+ }
15
+ return result;
16
+ }
17
+ function hashId(input, length = 5) {
18
+ const hash = Bun.hash(input);
19
+ const base62 = toBase62(BigInt(hash));
20
+ const padded = base62.padStart(length, "0");
21
+ return padded.slice(0, length);
22
+ }
23
+ export {
24
+ hashId
25
+ };