Skip to content

Commit 95dcbac

Browse files
committed
feat: add tests, 98% coverage, handle sync/async, insane types.. and more
Signed-off-by: tunnckoCore <5038030+tunnckoCore@users.noreply.github.com>
1 parent 5e673a1 commit 95dcbac

File tree

8 files changed

+1092
-62
lines changed

8 files changed

+1092
-62
lines changed

new-src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function createValidationError<ErrorKindNames = never>(
8282
} as const;
8383
}
8484

85-
export function createInternalError(msg: string, cause?: Error) {
85+
export function createInternalError(msg: string, cause?: any) {
8686
return {
8787
kind: "UNKNOWN_ERROR" as const,
8888
message: msg,

new-src/index.ts

Lines changed: 16 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import {
33
type ErrorsMapResolved,
44
type ResolveErrorKindNames,
55
} from "./errors";
6+
import type { ConditionalAsync } from "./is-promise";
67

78
import type {
89
AnySchema,
910
InferSchemaInput,
10-
InferSchemaInputSafe,
1111
InferSchemaOutput,
1212
InferSchemaOutputSafe,
13-
IsPromise,
1413
Prettify,
1514
ProcedureOptions,
1615
SpreadTuple,
@@ -30,8 +29,7 @@ export function zagora() {
3029
}
3130

3231
export class Zagora<
33-
TIsHandlerAsync,
34-
THandlerFn,
32+
THandlerFn extends (...args: any[]) => unknown,
3533
TContext extends any | undefined = undefined,
3634
TInputSchema extends AnySchema | undefined = undefined,
3735
TOutputSchema extends AnySchema | undefined = undefined,
@@ -45,14 +43,7 @@ export class Zagora<
4543

4644
input<TInput extends AnySchema>(
4745
inputSchema: TInput,
48-
): Zagora<
49-
TIsHandlerAsync,
50-
THandlerFn,
51-
TContext,
52-
TInput,
53-
TOutputSchema,
54-
TErrorsMap
55-
> {
46+
): Zagora<THandlerFn, TContext, TInput, TOutputSchema, TErrorsMap> {
5647
return new Zagora({
5748
...this.def,
5849
inputSchema,
@@ -61,14 +52,7 @@ export class Zagora<
6152

6253
output<TOutput extends AnySchema>(
6354
outputSchema: TOutput,
64-
): Zagora<
65-
TIsHandlerAsync,
66-
THandlerFn,
67-
TContext,
68-
TInputSchema,
69-
TOutput,
70-
TErrorsMap
71-
> {
55+
): Zagora<THandlerFn, TContext, TInputSchema, TOutput, TErrorsMap> {
7256
return new Zagora({
7357
...this.def,
7458
outputSchema,
@@ -77,14 +61,7 @@ export class Zagora<
7761

7862
context<TNewContext>(
7963
initialContext?: TNewContext,
80-
): Zagora<
81-
TIsHandlerAsync,
82-
THandlerFn,
83-
TNewContext,
84-
TInputSchema,
85-
TOutputSchema,
86-
TErrorsMap
87-
> {
64+
): Zagora<THandlerFn, TNewContext, TInputSchema, TOutputSchema, TErrorsMap> {
8865
return new Zagora({
8966
...this.def,
9067
initialContext,
@@ -93,14 +70,7 @@ export class Zagora<
9370

9471
errors<TErrors extends Record<string, AnySchema>>(
9572
errorsMap: TErrors & UppercaseKeys<TErrors>,
96-
): Zagora<
97-
TIsHandlerAsync,
98-
THandlerFn,
99-
TContext,
100-
TInputSchema,
101-
TOutputSchema,
102-
TErrors
103-
> {
73+
): Zagora<THandlerFn, TContext, TInputSchema, TOutputSchema, TErrors> {
10474
return new Zagora({
10575
...this.def,
10676
errorsMap,
@@ -122,14 +92,10 @@ export class Zagora<
12292
arg: InferSchemaOutput<TInputSchema>,
12393
) => any
12494
: (options: Prettify<ProcedureOptions<TContext, TErrorsMap>>) => any,
125-
TReturn = ReturnType<TFn>,
126-
TIsAsync extends boolean = IsPromise<TReturn>,
127-
>(
128-
fn: TFn,
129-
): Zagora<TIsAsync, TFn, TContext, TInputSchema, TOutputSchema, TErrorsMap> {
95+
>(fn: TFn): Zagora<TFn, TContext, TInputSchema, TOutputSchema, TErrorsMap> {
13096
return new Zagora({
13197
...this.def,
132-
handler: fn as any,
98+
handler: fn,
13399
});
134100
}
135101

@@ -168,19 +134,14 @@ export class Zagora<
168134

169135
type TResolvedResult = Awaited<ReturnType<typeof procedure>>;
170136

171-
type TResult = TIsHandlerAsync extends true
172-
? Promise<
173-
ZagoraResult<
174-
InferSchemaOutputSafe<TOutputSchema>,
175-
ErrorsMapResolved<TErrorsMap>,
176-
TResolvedResult
177-
>
178-
>
179-
: ZagoraResult<
180-
InferSchemaOutputSafe<TOutputSchema>,
181-
ErrorsMapResolved<TErrorsMap>,
182-
TResolvedResult
183-
>;
137+
type TResult = ConditionalAsync<
138+
ReturnType<THandlerFn>,
139+
ZagoraResult<
140+
InferSchemaOutputSafe<TOutputSchema>,
141+
ErrorsMapResolved<TErrorsMap>,
142+
TResolvedResult
143+
>
144+
>;
184145

185146
return procedure as TInputSchema extends AnySchema
186147
? InferSchemaInput<TInputSchema> extends readonly any[]

new-src/is-promise.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Detects the literal type `any`.
3+
*
4+
* This helper type returns `true` if the type parameter `T` is literally the `any` type,
5+
* and `false` otherwise. Uses the `0 extends 1 & T` trick which only passes for `any`.
6+
*/
7+
export type IsAny<T> = 0 extends 1 & T ? true : false;
8+
9+
/**
10+
* Returns `true` if the supplied value type `V` is a concrete promise.
11+
*
12+
* ## Behavior:
13+
* - `string | number | { … }` → `false`
14+
* - `{ … } | undefined | null | void` → `false`
15+
* - `new Promise(...)` → `true`
16+
* - `Promise.resolve(...)` → `true`
17+
* - `async () => …` return type → `true`
18+
* - **explicit** `ReturnType<void>` → `false`
19+
* - `any` → `false`
20+
* - `Promise<any>` → `true`
21+
*
22+
* ## Special Notes
23+
* - The literal value `any` is explicitly treated as "not a promise"
24+
* - However, `Promise<any>` IS detected as a promise (since we know it's wrapped)
25+
* - Uses array wrapper `[T]` to prevent distributive conditional types
26+
*
27+
* @template V - The value type you want to test
28+
*/
29+
export type IsPromise<V> = IsAny<V> extends true
30+
? false // Bare `any` → treat as non-promise
31+
: [V] extends [Promise<any>]
32+
? true // Explicitly Promise<T> → is a promise
33+
: [V] extends [PromiseLike<any>]
34+
? true // PromiseLike<T> → is a promise
35+
: [Awaited<V>] extends [V]
36+
? false // Awaiting V gives us V → not a promise
37+
: IsAny<Awaited<V>> extends true
38+
? false // If Awaited<V> is any, we can't tell → treat as non-promise
39+
: true; // Awaiting V gives us something different → is a promise
40+
41+
/**
42+
* Conditionally wraps a result type in a Promise based on whether the input type is a Promise.
43+
*
44+
* ## Use Case
45+
* Perfect for functions that should return `Promise<Result>` for async handlers
46+
* and `Result` for sync handlers.
47+
*
48+
* ## Behavior:
49+
* - `ConditionalAsync<Promise<T>, Result>` → `Promise<Result>`
50+
* - `ConditionalAsync<T, Result>` → `Result`
51+
* - `ConditionalAsync<any, Result>` → `Result` (bare `any` is treated as sync)
52+
* - `ConditionalAsync<Promise<any>, Result>` → `Promise<Result>` (Promise<any> is treated as async)
53+
*
54+
* ## Example:
55+
* ```ts
56+
* type Handler<T> = () => T;
57+
* type CallableResult<T> = ConditionalAsync<T, Result<Awaited<T>>>;
58+
*
59+
* // async () => string → ReturnType is Promise<string> → Promise<Result<string>>
60+
* // () => string → ReturnType is string → Result<string>
61+
* // () => Promise<T> → ReturnType is Promise<T> → Promise<Result<T>>
62+
* ```
63+
*
64+
* @template T - The type to check (typically a function's return type)
65+
* @template Result - The result type to wrap conditionally
66+
*/
67+
export type ConditionalAsync<T, Result> = IsPromise<T> extends true
68+
? Promise<Result> // Is a promise → wrap in Promise
69+
: Result; // Not a promise (including bare `any`) → return as-is

new-src/types.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
ValidationError,
88
} from "./errors";
99

10+
export * from "./is-promise";
11+
1012
export type Schema<I, O = I> = StandardSchemaV1<I, O>;
1113

1214
export type AnySchema = Schema<any, any>;
@@ -29,11 +31,11 @@ export type InferSchemaInput<T extends AnySchema> = T extends StandardSchemaV1<
2931

3032
export type InferSchemaOutputSafe<T> = T extends AnySchema
3133
? InferSchemaOutput<T>
32-
: any;
34+
: unknown;
3335

3436
export type InferSchemaInputSafe<T> = T extends AnySchema
3537
? InferSchemaInput<T>
36-
: any;
38+
: unknown;
3739

3840
export type UppercaseKeys<T> = {
3941
[K in keyof T as Uppercase<string & K>]: T[K];
@@ -44,7 +46,15 @@ export type Prettify<T> = {
4446
} & {};
4547

4648
export type IsOptional<T> = undefined extends T ? true : false;
47-
export type IsPromise<T> = T extends Promise<any> ? true : false;
49+
export type IsPromise<T> = T extends Promise<infer _>
50+
? T extends string
51+
? false
52+
: T extends number
53+
? false
54+
: number
55+
: T extends any
56+
? string
57+
: false;
4858

4959
// export type DefinedErrorsUnion<TErrorsMap> = TErrorsMap extends Record<
5060
// string,

0 commit comments

Comments
 (0)