Search
⌘K

Ts Result

Result pattern, type-only version, with Typescript Types as source.

Example Usage

Result is just a typescript type, that is a discriminated union on "status", with this shape:

type Result<TSuccessData, TErrorCodes> = (
  | { 
      status: 'success'; 
      data: TSuccessData;
    }
  | { 
      status: 'error'; 
      code: TErrorCodes | 'UNKNOWN_ERROR';
      message: string;
  }
)

Basic Usage

import type { Result } from './ts-result';

// 1. create type

type Fn1Result = Result<
  // success.data
  { name: string; }, 
  // error.code
  'FETCH_FAILED' | 'INVALID_DATA_FROM_API'
>;

// 2. implement a fn that return that 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...
    
    // define the expected success.data shape schema
    const dataSchema = z.object({ name: z.string() });

    // 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_FROM_API',
        message: 'Invalid data, maybe the API return type is changed and you need to adapt your code',
      }
    }

    // if is valid
    return {
      status: 'success',
      data: parsed.data,
    };

  }
  catch (error) {
    // for error unexpected

    // NOTE: 
    // Wait? I haven't specified that the error branch can have "UNKNOWN_ERROR"??
    // code "UNKNOWN_ERROR" is hardcoded in the Result definition so you don't need to add it

    return {
      status: 'error',
      code: 'UNKNOWN_ERROR',
      message: 'Something went wrong',
    };

  }

};

// 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_FROM_API') {
      console.log(result.message);
    }
    else /*if (result.code === 'UNKNOWN_ERROR') */ {
      console.log(result.message);
    }

    return;
  }

  // if success...
  const data = result.data;
  {
    name: string
  }

}

ResultAlwaysSuccess

Use this if you know that error is not possible, or if you want to treat error as success.

import type { ResultAlwaysSuccess } from './ts-result';

// 1. create type

type Fn2Result = ResultAlwaysSuccess<{ name: string; }>;

// 2. use

const fn2 = async (): Promise<Fn2Result> => {

  // ts force you to return a value with status === 'success'

  // ... do your work

  return {
    status: 'success',
    data: { name: 'John Doe' },
  };
};

Infer Types and Wrap Types

import type { Result, InferResultSuccess, InferResultError } from './ts-result';

// 1. create first result

type SubFnResult = Result<
  { name: string; }, 
  'FETCH_FAILED'
>;

type SubFnResultSuccess = InferResultSuccess<SubFnResult>;
type SubFnResultError = InferResultError<SubFnResult>;

const subFn = async (): Promise<SubFnResult> => {
  // omitted
}

// 2. Create a new result that extends the first result

type WrappedResult = Result<
  SubFnResultSuccess['data'],
  "INVALID_INPUT" | SubFnResultError['code']
>;

// 4. use


const wrappedFn = async (text: string): Promise<WrappedResult> => {

  try {

    // if invalid input -> return `INVALID_INPUT` error code
    if (text.length < 3) {
      return {
        status: 'error',
        code: 'INVALID_INPUT',
        message: 'Invalid input',
      };
    }

    // if error in sub fn -> return sub fn error
    const subFnResult = await subFn();
    if (subFnResult.status === 'error') {
      return subFnResult;
    }

    // if success in sub fn -> return sub fn success
    return {
      status: 'success',
      data: subFnResult.data,
    };

  }
  catch (error) {
    // if error unexpected -> return `UNKNOWN_ERROR` error code
    return {
      status: 'error',
      code: 'UNKNOWN_ERROR',
      message: 'Something went wrong',
    };

  }
};

Add Zod Schema

import type { Result } from './ts-result';
import { z } from 'zod';

const successDataSchema = z.object({
  name: z.string(),
  age: z.number(),
})

type MyResult = Result<
  z.infer<typeof successDataSchema>, 
  'FETCH_FAILED' | 'INVALID_DATA_FROM_API'
>;

const fn = async (): Promise<MyResult> => {
  try {

    // do fetch
    const response = fetch(...);

    // if status not ok...
    if (!response.ok) {
      return {
        status: 'error',
        code: 'FETCH_FAILED',
        message: '...',
      }
    }

    // if status ok...

    // check that data has expected shape
    const parsed = successDataSchema.safeParse(await response.json());

    // if invalid data
    if (!parsed.success) {
      return {
        status: 'error',
        code: 'INVALID_DATA_FROM_API',
        message: '...',
      }
    }

    // if valid data
    return {
      status: 'success',
      data: parsed.data,
    }
  }
  catch (error) {

    // if unexpected error
    return {
      status: 'error',
      code: 'UNKNOWN_ERROR',
      message: '...',
    }
  }
}

Dependencies

No dependencies

Auto Install

npx shadcn@latest add https://shadcn-registry-ts.vercel.app/r/util-ts-result.json

Manual Install

ts-result.ts
/** Type creator used to create a Result `success` type */
type ResultSuccess<TData> = {
  status: 'success';
  data: TData;
};
/** Type creator used to create a Result `error` type */
type ResultError<TCode extends string> = {
  status: 'error';
  code: TCode | 'UNKNOWN_ERROR',
  message: string;
};

/** 
 * Type creator used to create a Result type.  
 * You must pass two generics:
 * - the first will be the `data` prop of `success` path.
 * - the second will be the `code` prop of `error` path.
 * */
export type Result<TSuccessData, TErrorCode extends string> = ResultSuccess<TSuccessData> | ResultError<TErrorCode>;

/** 
 * Create a Result type that cannot have `error` path.
 * You must pass one generic:
 * - will be the `data` prop of `success` path.
 * */
export type ResultAlwaysSuccess<TSuccessData> = ResultSuccess<TSuccessData>;


/** Infer the `success` branch of a Result type */
export type InferResultSuccess<TResult> = Extract<TResult, { status: 'success'; }>;

/** Infer the `error` branch of a Result type */
export type InferResultError<TResult> = Extract<TResult, { status: 'error'; }>;

Test

No test

Command Palette

Search for a command to run...