Search
⌘K

Ts Result Zod

Result pattern with Zod instead of Typescript Types a source.

Example Usage

Basic Usage


import z from 'zod';

import {
  schemaResult,
  schemaResultSuccess,
  schemaResultError,
  type InferResult,
  type InferResultSuccess,
  type InferResultError,
} from './ts-result-zod';

const schemaFn1Result = schemaResult(
    schemaResultSuccess(z.object({ name: z.string() })),
    schemaResultError(['FETCH_FAILED', 'UNKNOWN_ERROR'] as const),
  );

type Fn1Result = InferResult<typeof schemaFn1Result>;

const fn1 = async (): Promise<Fn1Result> => {

  // ts force you to return a discriminated union

  try {
    const result = await fetch('https://api.example.com');

    // for error not unexpected
    if (!result.ok) {
      return {
        status: 'error',
        code: 'FETCH_FAILED',
        message: 'Fetch status is not ok: ' + result.status,
      };
    }

    // for success
    return {
      status: 'success',
      data: await result.json(),
    };
  }
  catch (error) {
    // for error unexpected
    return {
      status: 'error',
      code: 'UNKNOWN_ERROR',
      message: 'Something went wrong with fetch',
    };

  }

};

Infer Types

import z from 'zod';

import {
  schemaResult,
  schemaResultSuccess,
  schemaResultError,
  type InferResult,
  type InferResultSuccess,
  type InferResultError,
} from './ts-result-zod';

const simpleResult = schemaResult(
  schemaResultSuccess(z.object({ name: z.string(), age: z.number() })),
  schemaResultError(['MY_ERROR_CODE', 'UNKNOWN_ERROR'] as const),
);

type SimpleResult = InferResult<typeof simpleResult>;
// ⏬
{
  status: "success";
  data: {
      name: string;
      age: number;
  };
} | {
  status: "error";
  code: "UNKNOWN_ERROR" | "MY_ERROR_CODE";
  message: string;
}

type SimpleResultSuccess = InferResultSuccess<typeof simpleResult>;
// ⏬
{
  status: "success";
  data: {
      name: string;
      age: number;
  };
}


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';

// define first result
const simpleResult = schemaResult(
  schemaResultSuccess(z.object({ name: z.string(), age: z.number() })),
  schemaResultError(['MY_ERROR_CODE', 'UNKNOWN_ERROR'] as const),
);

// define second result that wrap the first
const wrappedResult = schemaResult(
  schemaResultSuccess(
    z.object({
      simpleData: getResultData(simpleResult).successData,
      extraInfo: z.string(),
    }),
  ),
  schemaResultError([
    'INVALID_INPUT',
    ...getResultData(simpleResult).errorCodes
  ] as const),
);

// 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;
}


// 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 = {
  SuccessData: z.ZodSchema;
  ErrorCodes: readonly [string, ...string[]],
};

/** 
 * Create a Result Success Zod Schema.  
 * See {@link schemaResult}
 * */
export const schemaResultSuccess = <DataSchema extends Base['SuccessData']>(dataSchema: DataSchema) => z.object({
  status: z.literal('success'),
  data: dataSchema,
});

/** 
 * Create a Result Error Zod Schema.  
 * See {@link schemaResult}
 * */
export const schemaResultError = <ErrorCodes extends Base['ErrorCodes']>(errorCodes: ErrorCodes) => z.object({
  status: z.literal('error'),
  code: z.enum(errorCodes),
  message: z.string(),
});

/** Create a Result Zod Schema 
 * @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 = <
  S extends Base['SuccessData'],
  E extends Base['ErrorCodes'],
>(
  schemaSuccess: ReturnType<typeof schemaResultSuccess<S>>,
  schemaError: ReturnType<typeof schemaResultError<E>>,
) => z.discriminatedUnion('status', [
  schemaSuccess,
  schemaError,
]);

/** Infer Types of a Result. Contains both `success` and `error`.
 * @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<R extends ReturnType<typeof schemaResult>> = z.infer<R>;

/** Infer Types of a Result Success
 * @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<R extends ReturnType<typeof schemaResult>> = Extract<InferResult<R>, { status: 'success'; }>;

/** Infer Types of a Result Error
 * @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<R extends ReturnType<typeof schemaResult>> = Extract<InferResult<R>, { status: 'error'; }>;


const getResultSuccess = <
  S extends Base['SuccessData'],
  E extends Base['ErrorCodes'],
>(
  result: ReturnType<typeof schemaResult<S, E>>
) => result.options[0];

const getResultError = <
  S extends Base['SuccessData'],
  E extends Base['ErrorCodes'],
>(
  result: ReturnType<typeof schemaResult<S, E>>
) => result.options[1];


/**
 * Extract Data from a Result Zod Schema
 * @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 = <
  S extends Base['SuccessData'],
  E extends Base['ErrorCodes'],
>(
  result: ReturnType<typeof schemaResult<S, E>>
) => {
  return {
    success: getResultSuccess(result),
    successData: getResultSuccess(result).shape.data,
    error: getResultError(result),
    errorCodes: getResultError(result).shape.code.options,
  };
};

Test

No test

Command Palette

Search for a command to run...