0

I have a function called createAction() with multiple overloads. Each overload takes an (Action) or an (Action, TObject) as parameters, and returns [Action] or [Action, TObject] respectively:

enum Action {
  DataAction,
  DatalessAction,
  // ...
}

interface CreateAction {
  (action: Action.DataAction, data: { str: string }): [typeof action, typeof data];
  (action: Action.DatalessAction): [typeof action];
  // ...
}

// this fails with Property '0' is missing in type (object | Action)[]
export const createAction: CreateAction = function(action: Action, data?: object) {
  if (data) {
    return [action, data];
  }

  return [action];
}

// these both succeed
var x = createAction(Action.DataAction, { str: 'foo' }),
    y = createAction(Action.DatalessAction);

Alternatively, this fails with Property '1' is missing in type '[Action]' but required in type [Action.DataAction, { str: string }]:

export const createAction: CreateAction = function(action: Action, data?: object): 
  [typeof action] | [typeof action, Exclude<typeof data, undefined>] 
{
  if (data !== undefined) {
    return [action, data];
  }

  return [action];
}

and this fails with Type 'Action' is not assignable to type 'Action.DataAction':

export const createAction: CreateAction = function(action: Action, data?: object): 
  [typeof action, Exclude<typeof data, undefined>?] 
{
  if (data !== undefined) {
    return [action, data];
  }

  return [action];
}

Is it possible to do what I want? Here's the code in TypeScript playground.

1 Answer 1

1

TypeScript function types are often incompatible with function overloads and I've personally never found a good way around this. In playing with your code sample, I found that explicitly defining the return types yields a more descriptive error:

export const createAction: CreateAction = function(action: Action, data?: object) {
  if (data) {
    return <[Action.DataAction, { str: string }]>[action, data];
  }

  return <[Action.DatalessAction]>[action];
}

Compiler error: Type '(action: Action, data?: object | undefined) => [Action.DataAction, { str: string; }] | [Action.DatalessAction]' is not assignable to type 'CreateAction'. Type '[Action.DataAction, { str: string; }] | [Action.DatalessAction]' is not assignable to type '[Action.DataAction, { str: string; }]'. Property '1' is missing in type '[Action.DatalessAction]' but required in type '[Action.DataAction, { str: string; }]'.

Indicating that a return type union is not equivalent to the individual return types of a function union.


I'm not a typescript contributor and don't know the internal workings of the compiler, but based on my personal experience, I believe a function type in TypeScript is not the same as, and cannot be made to match with, a function union type (i.e. an overloaded function) which CreateAction is.

One brute force solution is to just cast the fuction to CreateAction like this:

export const createAction: CreateAction = <CreateAction>function(action: Action, data?: object) {
  // ...
}

Which clears the compiler error, however you lose type checking safety. But you can partially compensate for that by first adding a cast to the function type you expect:

type CreateActionCompatible = (action: Action, data?: object) => [Action] | [Action, object];
export const createAction: CreateAction = <CreateAction><CreateActionCompatible>function(action: Action, data?: object) {
  // ...
}

Now if you try to assign a function with an incompatible signature you'll get a compiler error which I believe you want:

export const createAction: CreateAction = <CreateAction><CreateActionCompatible>function(action: boolean, data?: object) {
  // ...
}

Compiler error: Conversion of type '(action: boolean, data?: object | undefined) => [boolean, object] | [boolean]' to type 'CreateActionCompatible' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Types of parameters 'action' and 'action' are incompatible. Type 'Action' is not comparable to type 'boolean'.

It's not pretty, but sometimes our compilers just aren't smart enough to know what we want.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.