Search
⌘K

Ts Result Zod

Result pattern, runtime version, with Zod instead of Typescript Types as source.

Example Usage

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...