adriengibrat/ts-custom-error

customErrorFactory does not act like CustomError class

ashleyw opened this issue · 2 comments

Hi!

I've noticed some inconsistencies between using customErrorFactory and the CustomError class directly. When using the factory, the return type resolves as just a basic Error instance, i.e. it does not contain any extra properties:

import { CustomError, customErrorFactory } from 'ts-custom-error';

interface Props {
  env: string;
  message: string;
}

export const EnvironmentVariableMissingViaFactory = customErrorFactory<Props>(
  function EnvironmentVariableMissing(env: string) {
    this.message = `Hello world`;
    this.env = env;
  },
);

class EnvironmentVariableMissingViaClass extends CustomError {
  constructor(public env: string, message: string = `Hello world`) {
    super(message);
  }
}


// I think this `InstanceType<typeof ...>` signature is correct?
function myFunc1(error: InstanceType<typeof EnvironmentVariableMissingViaFactory>) {
  // typeof error === Error

  return {
    msg: error.message,
    env: error.env,
  };

  // => ❌ Property 'env' does not exist on type 'Error'
}

function myFunc2(error: EnvironmentVariableMissingViaClass) {
  // typeof error === EnvironmentVariableMissingViaClass

  return {
    msg: error.message,
    env: error.env,
  };

  // {
  //    msg: (property) Error.message: string,
  //    env: (property) EnvironmentVariableMissingViaClass.env: string
  //  }
}

const myErr = new EnvironmentVariableMissingViaFactory('PORT');

function myFunc3(error: typeof myErr) {
  // typeof error === CustomErrorInterface & Props

  return {
    msg: error.message,
    env: error.env,
  };

  // {
  //    msg: (property) message: string
  //    env: (property) Props.env: string
  //  }
}

As a real-world example, I'm using Ava for testing, which has a throws<T extends Error>() method:

const key = 'PORT';

const error = t.throws<InstanceType<typeof EnvironmentVariableMissingViaFactory>>(() => {
  GetEnv<number>(key);
}, EnvironmentVariableMissingViaFactory);

t.is(error.env, key);
// => ❌ Property 'env' does not exist on type 'Error'
const key = 'PORT';

const error = t.throws<EnvironmentVariableMissingViaClass>(() => {
  GetEnv<number>(key);
}, EnvironmentVariableMissingViaClass);

t.is(error.env, key);
// ✅ EnvironmentVariableMissingViaClass.env: string

Is this behaviour intentional? Isn't it possible for customErrorFactory to return a CustomError instance to ensure both options are consistent?

Thanks!

  • Typescript: 3.4.5
  • ts-custom-error: 3.0.0

Thanks for your feedback, with great details ;)

I think this InstanceType<typeof ...> signature is correct?

As your last example myFunc3 shows, the actual type of an Error instance returned by constructor build with customErrorFactory is CustomErrorInterface & Props where CustomErrorInterface is just anythig that extends Error.

You can create a type to use in signatures:

type EnvironmentVariableMissingErrorViaFactory = CustomErrorInterface & Props
function myFunc1(error: EnvironmentVariableMissingErrorViaFactory)...

N.B. unfortunately InstanceType seems not to be helpfull in this case:

type EnvironmentVariableMissingViaFactory2 = InstanceType<CustomErrorConstructor<Props>> // e.q. Error
function myFunc1(error: EnvironmentVariableMissingViaFactory2)...

Is this behaviour intentional?

yes, and no. I looked for typescript trick to express a 'dynamic' interface using generics (something like
CustomError<Props> === CustomError & Props) without finding one, so I came up with returning CustomErrorInterface & Props... and the interface composition can't be avoided IMO because the actual interface is created inside the factory.

The reason why I used CustomErrorInterface & Props and not CustomError & Props was just to avoid unecessary ties between factory & class because I assumed one will only use one or the other.

Isn't it possible for customErrorFactory to return a CustomError instance to ensure both options are consistent?

The only obvious solution I can think of is to replace CustomErrorInterface in https://github.com/adriengibrat/ts-custom-error/blob/master/src/factory.ts#L3 by an import of CustomError.

You'll still have to create a custom type when using factory, but at least class & factory will use the same base type...

type EnvironmentVariableMissingErrorViaFactory = CustomError & Props
function myFunc1(error: EnvironmentVariableMissingErrorViaFactory)...

Was my explaination helpfull?

Do you think using CustomError as a base type in factory.ts is worth a fix ?

Closing, feel free to reopen if needed.