Skip to content

Commit 67b8124

Browse files
committed
chore: basic types and helpers
1 parent 0da1911 commit 67b8124

File tree

19 files changed

+408
-22
lines changed

19 files changed

+408
-22
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"lint:fix": "biome check --write",
2020
"new:package": "NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
2121
"prepublish": "pnpm run build",
22-
"test": "echo \"Error: no test specified\" && exit 1"
22+
"test": "vitest"
2323
},
2424
"license": "MIT",
2525
"devDependencies": {

packages/types/package.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@
1919
},
2020
"typesVersions": {
2121
"*": {
22-
"import": ["./dist/index.d.ts"],
23-
"require": ["./dist/index.d.cts"]
22+
"import": [
23+
"./dist/index.d.ts"
24+
],
25+
"require": [
26+
"./dist/index.d.cts"
27+
]
2428
}
2529
},
26-
"files": ["dist"],
30+
"files": [
31+
"dist"
32+
],
33+
"sideEffects": false,
2734
"scripts": {
2835
"build": "tsup"
2936
},
30-
"dependencies": {},
37+
"dependencies": {
38+
"neverthrow": "^8.2.0",
39+
"type-fest": "^4.41.0"
40+
},
3141
"devDependencies": {
3242
"tsup": "^8.5.0",
3343
"typescript": "^5.6.3"

packages/types/src/errors.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { err, errAsync, type Result, type ResultAsync } from 'neverthrow';
2+
3+
export class ResultAwareError extends Error {
4+
/**
5+
* @internal
6+
*/
7+
asResultAsync(): ResultAsync<never, typeof this> {
8+
return errAsync(this);
9+
}
10+
11+
/**
12+
* @internal
13+
*/
14+
asResult(): Result<never, typeof this> {
15+
return err(this);
16+
}
17+
18+
/**
19+
* @internal
20+
*/
21+
static from<T extends typeof ResultAwareError>(
22+
this: T,
23+
message: string,
24+
): InstanceType<T>;
25+
static from<T extends typeof ResultAwareError>(
26+
this: T,
27+
cause: unknown,
28+
): InstanceType<T>;
29+
static from<T extends typeof ResultAwareError>(
30+
this: T,
31+
args: unknown,
32+
): InstanceType<T> {
33+
if (args instanceof Error) {
34+
// biome-ignore lint/complexity/noThisInStatic: intentional
35+
const message = this.formatMessage(args);
36+
// biome-ignore lint/complexity/noThisInStatic: intentional
37+
return new this(message, { cause: args }) as InstanceType<T>;
38+
}
39+
// biome-ignore lint/complexity/noThisInStatic: intentional
40+
return new this(String(args)) as InstanceType<T>;
41+
}
42+
43+
static is<T extends typeof ResultAwareError>(
44+
this: T,
45+
error: unknown,
46+
): error is InstanceType<T> {
47+
// biome-ignore lint/complexity/noThisInStatic: intentional
48+
return error instanceof this;
49+
}
50+
51+
private static formatMessage(cause: Error): string {
52+
const messages: string[] = [];
53+
let currentError: unknown = cause;
54+
55+
while (currentError instanceof Error) {
56+
if ('errors' in currentError && Array.isArray(currentError.errors)) {
57+
// Handle AggregateError
58+
const inner = currentError.errors.map((e: unknown) =>
59+
e instanceof Error ? e.message : String(e),
60+
);
61+
messages.push(inner.join(', '));
62+
} else {
63+
messages.push(currentError.message);
64+
}
65+
currentError = currentError.cause;
66+
}
67+
68+
return messages.join(' due to ');
69+
}
70+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Unwraps the promise to allow resolving/rejecting outside the Promise constructor
3+
*/
4+
export class Deferred<T = void> {
5+
readonly promise: Promise<T>;
6+
resolve!: (value: T) => void;
7+
reject!: (reason?: unknown) => void;
8+
9+
constructor() {
10+
this.promise = new Promise<T>((resolve, reject) => {
11+
this.resolve = resolve;
12+
this.reject = reject;
13+
});
14+
}
15+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Err, Ok, Result } from 'neverthrow';
2+
import type { UnknownRecord } from 'type-fest';
3+
import { InvariantError } from './invariant';
4+
5+
function isObject(value: unknown): value is UnknownRecord {
6+
const type = typeof value;
7+
return value != null && (type === 'object' || type === 'function');
8+
}
9+
10+
export function assertError(error: unknown): asserts error is Error {
11+
// why not `error instanceof Error`? see https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1099
12+
// biome-ignore lint/suspicious/noPrototypeBuiltins: safe
13+
if (!isObject(error) || !Error.prototype.isPrototypeOf(error)) {
14+
throw error;
15+
}
16+
}
17+
18+
/**
19+
* Exhaustiveness checking for union and enum types
20+
* see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
21+
*/
22+
export function assertNever(
23+
x: never,
24+
message = `Unexpected object: ${String(x)}`,
25+
): never {
26+
throw new InvariantError(message);
27+
}
28+
29+
/**
30+
* Asserts that the given `Result<T, E>` is an `Ok<T, never>` variant.
31+
*/
32+
export function assertOk<T, E extends Error>(
33+
result: Result<T, E>,
34+
): asserts result is Ok<T, E> {
35+
if (result.isErr()) {
36+
throw new InvariantError(
37+
`Expected result to be Ok: ${result.error.message}`,
38+
);
39+
}
40+
}
41+
42+
/**
43+
* Asserts that the given `Result<T, E>` is an `Err<never, E>` variant.
44+
*/
45+
export function assertErr<T, E extends Error>(
46+
result: Result<T, E>,
47+
): asserts result is Err<T, E> {
48+
if (result.isOk()) {
49+
throw new InvariantError(`Expected result to be Err: ${result.value}`);
50+
}
51+
}
52+
53+
/**
54+
* Asserts that the given value is not `null`.
55+
*/
56+
export function assertNotNull<T>(value: T): asserts value is Exclude<T, null> {
57+
if (value === null) {
58+
throw new InvariantError('Expected value to be not null');
59+
}
60+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* The identity function returns the input value unchanged.
3+
*
4+
* @typeParam T - The type of the input and output.
5+
* @param value - The value to return.
6+
* @returns The same value that was passed in.
7+
*/
8+
export function identity<T>(value: T): T {
9+
return value;
10+
}
11+
12+
/**
13+
* Alias for the {@link identity} function.
14+
*/
15+
export const passthrough = identity;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from './assertions';
2+
export * from './Deferred';
3+
export * from './identity';
4+
export * from './invariant';
5+
export * from './never';
6+
export * from './refinements';
7+
export * from './typeguards';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* An error that occurs when a task violates a logical condition that is assumed to be true at all times.
3+
*/
4+
export class InvariantError extends Error {
5+
name = 'InvariantError' as const;
6+
}
7+
8+
/**
9+
* Asserts that the given condition is truthy
10+
* @internal
11+
*
12+
* @param condition - Either truthy or falsy value
13+
* @param message - An error message
14+
*/
15+
export function invariant(
16+
condition: unknown,
17+
message: string,
18+
): asserts condition {
19+
if (!condition) {
20+
throw new InvariantError(message);
21+
}
22+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { InvariantError } from './invariant';
2+
3+
export function never(message = 'Unexpected call to never()'): never {
4+
throw new InvariantError(message);
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { InvariantError } from './invariant';
2+
3+
/**
4+
* Refinement function to check if a value is not null or undefined.
5+
*/
6+
export function nonNullable<T>(value: T): Exclude<T, null | undefined> {
7+
if (value === null || value === undefined) {
8+
throw new InvariantError('Value should not be null or undefined');
9+
}
10+
return value as Exclude<T, null | undefined>;
11+
}
12+
13+
/**
14+
* Creates a refinement function to check if a value has a specific `__typename` in a union of types with `__typename`.
15+
*/
16+
export function expectTypename<
17+
T extends { __typename: string },
18+
Name extends T['__typename'],
19+
>(typename: Name) {
20+
return (value: T): Extract<T, { __typename: Name }> => {
21+
if (value.__typename !== typename) {
22+
throw new InvariantError(
23+
`Expected __typename to be "${typename}", but got "${value.__typename}"`,
24+
);
25+
}
26+
return value as Extract<T, { __typename: Name }>;
27+
};
28+
}

0 commit comments

Comments
 (0)