jabuj
jabuj

Reputation: 3659

Typescript conditional void parameter type

I was trying to implement a custom Error class, which would accept different kinds of data depending on the error code. I was pretty sure that was I wanted to do was too complex for typescript to infer, but, to my surprise, this worked:

const enum ERROR_CODES {
  E_AUTHORIZATION = 'Authorization error',
  E_REQUEST = 'Request failed',
  E_INVALID = 'Invalid data',
}
interface ERROR_TYPES {
  [ERROR_CODES.E_AUTHORIZATION]: string
  [ERROR_CODES.E_REQUEST]: { url: string; status: number }
  [ERROR_CODES.E_INVALID]: { action: string; field: string }
}

class MyError<TError extends ERROR_CODES> extends Error {
  constructor(
    public readonly code: TError,
    public readonly data: ERROR_TYPES[TError],
  ) { super(code) }
}

Now I can use it like this:

throw new MyError(ERROR_CODES.E_AUTHORIZATION, 'whatever')
throw new MyError(ERROR_CODES.E_AUTHORIZATION, { 
  operation: 'login',
  field: 'email',
})

This works fine. Another thing I wanted to do is to create such error codes that need no data. Since it is possible to define functions like this:

function foo(bar: void) {}
foo()

My next logical step was to write this:

const enum ERROR_CODES {
  // ...
  E_UNKNOWN = 'Unknown error'
}
interface ERROR_TYPES {
  // ...
  [ERROR_CODES.E_UNKNOWN]: void
}

Now typescript behaves in quite a strange way. If I write this:

throw new MyError(ERROR_CODES.E_UNKNOWN, undefined)

it works. If I write this:

throw new MyError(ERROR_CODES.E_UNKNOWN)

it says Expected 2 arguments, but got 1.. If I write something like this:

throw new MyError(ERROR_CODES.E_UNKNOWN, void 0)

which should basically be the same as the first example, then it says Expected 'undefined' and instead saw 'void'.. What is happening here and is it possible to make the second example work with just one argument?

Upvotes: 0

Views: 288

Answers (1)

jcalz
jcalz

Reputation: 330466

I can't reproduce "expected undefined but instead saw void".


Here's how I might approach what you're trying to do. To "optionally make a function parameter optional", I'd represent the parameter list as as rest tuple. Consider this type alias:

type UndefParamToOptional<T> = undefined extends T ? [T?] : [T];

The type UndefParamToOptional<string> is just [string], but UndeParamToOptional<string | undefined> is [string?]. That's a tuple with a single optional element corresponding to an optional function parameter. Then we can implement MyError like this:

class MyError<TError extends ERROR_CODES> extends Error {
  constructor(
    code: TError,
    ...[data]: UndefParamToOptional<ERROR_TYPES[TError]>
  );
  constructor(public readonly code: TError, public readonly data: ERROR_TYPES[TError]) {
    super(code);
    this.data = data;
  }
}

I'm using a single overload signature to show how you intend to call it, while leaving the implementation signature unchanged.

Now this should behave as you expect:

throw new MyError(ERROR_CODES.E_UNKNOWN); // okay

Hope that helps. Good luck!

Link to code

Upvotes: 1

Related Questions