Ts Result Zod
Result pattern, runtime version, with Zod instead of Typescript Types as source.
Example Usage
Tip
This runtime Result pattern is built by creating a runtime validation schema and then inferring the type from it. This pattern is good when you also need to do unit testing, because you can validate the schema at runtime inside tests.
If you don't need to do unit testing, you can consider using Ts Result pattern, that is simpler.
Result is a Zod Schema with a discriminated union on "status", that is equivalent to this manual code:
import { z } from 'zod';
const myResult = z.discriminatedUnion("status", [
z.object({
status: z.literal("success"),
data: z.object({ name: z.string() }),
}),
z.object({
status: z.literal("error"),
code: z.enum(["FETCH_FAILED", "INVALID_DATA", "UNKNOWN_ERROR"] as const),
message: z.string(),
})
]);
type MyResult = z.infer<typeof myResult>;
// ⏬
| {
status: 'success';
data: {
name: string;
}
}
| {
status: 'error';
code: 'FETCH_FAILED' | 'INVALID_DATA' | 'UNKNOWN_ERROR';
message: string;
}that can be created with
const schemaMyResult = schemaResult(
schemaResultSuccess(
// pass only the `data` schema
z.object({ name: z.string() })
),
schemaResultError(
// pass only the `code` options
['FETCH_FAILED', 'INVALID_DATA', 'UNKNOWN_ERROR'] as const
),
);
type MyResult = InferResult<typeof schemaMyResult>;Basic Usage
import z from 'zod';
import {
schemaResult,
schemaResultSuccess,
schemaResultError,
type InferResult,
getResultData,
} from './ts-result-zod';
// 1. create a result + infer type
const schemaFn1Result = schemaResult(
schemaResultSuccess(z.object({ name: z.string() })),
schemaResultError(['FETCH_FAILED', 'INVALID_DATA', 'UNKNOWN_ERROR'] as const),
);
type Fn1Result = InferResult<typeof schemaFn1Result>;
// 2. implement a function that returns the result
const fn1 = async (): Promise<Fn1Result> => {
try {
// do fetch
const response = await fetch('https://api.example.com');
// if fetch status is not ok...
if (!response.ok) {
return {
status: 'error',
code: 'FETCH_FAILED',
message: 'Fetch status is not ok: ' + response.status,
};
}
// if fetch status is ok...
// extract the expected success.data shape schema
const dataSchema = getResultData(schemaFn1Result).successData;
// check that data has expected shape
const parsed = dataSchema.safeParse(await response.json());
// if invalid data
if (!parsed.success) {
return {
status: 'error',
code: 'INVALID_DATA',
message: 'Invalid data, maybe the API return type is changed and you need to adapt your code',
};
}
// if valid data
return {
status: 'success',
data: parsed.data,
};
}
catch (error) {
// for error unexpected
return {
status: 'error',
code: 'UNKNOWN_ERROR',
message: 'Something went wrong with fetch',
};
}
};
// 3. use
// NOTE: in this orchestrator function we don't need to try catch
async function main() {
// run the fn
const result = await fn1();
// check the discriminated union on "status"...
// if error...
if (result.status === 'error') {
if (result.code === 'FETCH_FAILED') {
console.log(result.message);
}
else if (result.code === 'INVALID_DATA') {
console.log(result.message);
}
else /*if (result.code === 'UNKNOWN_ERROR') */ {
console.log(result.message);
}
return;
}
// if success...
const data = result.data;
{
name: string
}
}
Infer Types
import z from 'zod';
import {
schemaResult,
schemaResultSuccess,
schemaResultError,
type InferResult,
type InferResultSuccess,
type InferResultError,
} from './ts-result-zod';
// 1. create result
const simpleResult = schemaResult(
schemaResultSuccess(z.object({ name: z.string(), age: z.number() })),
schemaResultError(['MY_ERROR_CODE', 'UNKNOWN_ERROR'] as const),
);
// 2. infer types
// full type
type SimpleResult = InferResult<typeof simpleResult>;
// ⏬
{
status: "success";
data: {
name: string;
age: number;
};
} | {
status: "error";
code: "UNKNOWN_ERROR" | "MY_ERROR_CODE";
message: string;
}
// only success type
type SimpleResultSuccess = InferResultSuccess<typeof simpleResult>;
// ⏬
{
status: "success";
data: {
name: string;
age: number;
};
}
// only error type
type SimpleResultError = InferResultError<typeof simpleResult>;
// ⏬
{
status: "error";
code: "UNKNOWN_ERROR" | "MY_ERROR_CODE";
message: string;
}Wrap Result in an other Result
import z from 'zod';
import {
schemaResult,
schemaResultSuccess,
schemaResultError,
type InferResult,
getResultData,
} from './ts-result-zod';
// 1. define first result
const simpleResult = schemaResult(
schemaResultSuccess(z.object({ name: z.string(), age: z.number() })),
schemaResultError(['MY_ERROR_CODE', 'UNKNOWN_ERROR'] as const),
);
// 2. define second result that wrap the first
const wrappedResult = schemaResult(
schemaResultSuccess(
z.object({
simpleData: getResultData(simpleResult).successData,// this is the `data` schema
extraInfo: z.string(),
}),
),
schemaResultError([
'INVALID_INPUT',
...getResultData(simpleResult).errorCodes // this is the `code` options
] as const),
);
// 3. infer types of the wrapped result
type WrappedResult = InferResult<typeof wrappedResult>;
// ⏬
{
status: "success";
data: {
simpleData: {
name: string;
age: number;
};
extraInfo: string;
};
} | {
status: "error";
code: "UNKNOWN_ERROR" | "MY_ERROR_CODE" | "INVALID_INPUT";
message: string;
}
// 4. use it
const fnWrapped = async (): Promise<WrappedResult> => {
// ...
}
Dependencies
npmzod
Auto Install
npx shadcn@latest add https://shadcn-registry-ts.vercel.app/r/util-ts-result-zod.json
Manual Install
ts-result-zod.ts
import z from "zod";
type Base = {
SuccessDataSchema: z.ZodSchema;
ErrorCodes: readonly [string, ...string[]],
};
/**
* Create a Result Zod Schema `success` branch.
* See {@link schemaResult}
* */
export const schemaResultSuccess = <TSuccessDataSchema extends Base['SuccessDataSchema']>(
/** zod schema of `data` prop @example z.object({ name: z.string() }) */
dataSchema: TSuccessDataSchema
) => z.object({
status: z.literal('success'),
data: dataSchema,
});
/**
* Create a Result Zod Schema `error` branch.
* See {@link schemaResult}
* */
export const schemaResultError = <TErrorCode extends Base['ErrorCodes']>(
/** list of error codes @example ['FETCH_FAILED', 'OTHER_ERROR'] as const */
errorCodes: TErrorCode
) => z.object({
status: z.literal('error'),
code: z.enum(errorCodes),
message: z.string(),
});
/** Create a Result Zod Schema, containing both `success` and `error` branches.
* @example
* const result = schemaResult(
* schemaResultSuccess(z.object({ name: z.string() })),
* schemaResultError(['FETCH_FAILED'] as const)
* );
*
* type Result = InferResult<typeof result>;
* // ⏬
* type Result = {
* status: 'success';
* data: {
* name: string
* }
* } | {
* status: 'error';
* code: 'FETCH_FAILED';
* message: string
* }
*
* const fn = async (): Promise<Result> => {
* // ...
* }
* */
export const schemaResult = <
TSuccessDataSchema extends Base['SuccessDataSchema'],
TErrorCode extends Base['ErrorCodes'],
>(
/** zod schema of `success` branch. created by {@link schemaResultSuccess} */
schemaSuccess: ReturnType<typeof schemaResultSuccess<TSuccessDataSchema>>,
/** zod schema of `error` branch. created by {@link schemaResultError} */
schemaError: ReturnType<typeof schemaResultError<TErrorCode>>,
) => z.discriminatedUnion('status', [
schemaSuccess,
schemaError,
]);
/**
* Infer Types of a Result Zod Schema.
* The input is a Result Zod Schema created by {@link schemaResult}.
* Returned type contains both `success` and `error` branches.
* @example
* const result = schemaResult(
* schemaResultSuccess(z.object({ name: z.string() })),
* schemaResultError(['FETCH_FAILED'] as const)
* );
*
* type Result = InferResult<typeof result>;
* // ⏬
* {
* status: 'success';
* data: {
* name: string
* }
* } | {
* status: 'error';
* code: 'FETCH_FAILED';
* message: string
* }
* */
export type InferResult<TResult extends ReturnType<typeof schemaResult>> = z.infer<TResult>;
/**
* Infer Types of a Result Zod Schema `success` branch.
* The input is a Result Zod Schema created by {@link schemaResult}.
* @example
* const result = schemaResult(
* schemaResultSuccess(z.object({ name: z.string() })),
* schemaResultError(['FETCH_FAILED'] as const)
* );
*
* type ResultSuccess = InferResultSuccess<typeof result>;
* // ⏬
* {
* status: 'success';
* data: {
* name: string
* }
* }
* */
export type InferResultSuccess<TResult extends ReturnType<typeof schemaResult>> = Extract<InferResult<TResult>, { status: 'success'; }>;
/**
* Infer Types of a Result Zod Schema `error` branch.
* The input is a Result Zod Schema created by {@link schemaResult}.
* @example
* const result = schemaResult(
* schemaResultSuccess(z.object({ name: z.string() })),
* schemaResultError(['FETCH_FAILED'] as const)
* );
*
* type ResultError = InferResultError<typeof result>;
* // ⏬
* {
* status: 'error';
* code: 'FETCH_FAILED';
* message: string
* }
* */
export type InferResultError<TResult extends ReturnType<typeof schemaResult>> = Extract<InferResult<TResult>, { status: 'error'; }>;
const getResultSuccessSchema = <
TSuccessDataSchema extends Base['SuccessDataSchema'],
TErrorCode extends Base['ErrorCodes'],
>(
result: ReturnType<typeof schemaResult<TSuccessDataSchema, TErrorCode>>
) => result.options[0];
const getResultErrorSchema = <
TSuccessDataSchema extends Base['SuccessDataSchema'],
TErrorCode extends Base['ErrorCodes'],
>(
result: ReturnType<typeof schemaResult<TSuccessDataSchema, TErrorCode>>
) => result.options[1];
/**
* Extract Data from a Result Zod Schema.
* The input is a Result Zod Schema created by {@link schemaResult}.
* @example
* const result = schemaResult(
* schemaResultSuccess(z.object({ name: z.string() })),
* schemaResultError(['FETCH_FAILED'] as const)
* );
*
* const resultData = getResultData(result);
* // ⏬
* {
* success: schemaResultSuccess(z.object({ name: z.string() })),
* successData: z.object({ name: z.string() }),
* error: schemaResultError(['FETCH_FAILED'] as const),
* errorCodes: ['FETCH_FAILED'] as const,
* }
*
* const wrappedResult = schemaResult(
* schemaResultSuccess(
* z.object({
* extraData: z.string(),
* otherResultData: getResultData(result).successData,
* })
* ),
* schemaResultError([
* 'NEW_ERROR_CODE',
* ...getResultData(result).errorCodes
* ] as const)
* );
*
*/
export const getResultData = <
TSuccessDataSchema extends Base['SuccessDataSchema'],
TErrorCode extends Base['ErrorCodes'],
>(
/** Result Zod Schema created by {@link schemaResult} */
result: ReturnType<typeof schemaResult<TSuccessDataSchema, TErrorCode>>
) => {
return {
/** full `success` zod schema */
success: getResultSuccessSchema(result),
/** `data` schema of `success` zod schema */
successData: getResultSuccessSchema(result).shape.data,
/** full `error` zod schema */
error: getResultErrorSchema(result),
/** `code` schema of `error` zod schema */
errorCodes: getResultErrorSchema(result).shape.code.options,
};
};
Test
No test
Command Palette
Search for a command to run...