Can you overload a function based on a destructured parameter?

This example code is somewhat contrived, but I'm trying to do something similar to it:

type ValueRequirement =
  | 'None'
  | 'Any String'
  | 'Any Number'
  | 'Another Widget\'s Value';

type Widget = {
  value: string
}

type WidgetValidationRule = {
  widget: Widget,
  valueRequirement: ValueRequirement,
}

function isANumber(value: string): boolean {
  throw new Error("Function not implemented."); // irrelevant implementation. Just trying to provide an example that can compile
}

function validate(widgetValidationRule: WidgetValidationRule): void {
  switch (widgetValidationRule.valueRequirement) {
    case 'None':
      return
    case 'Any Number':
      if (!isANumber(widgetValidationRule.widget.value)) {
        throw new Error('NaN');
      }
      return
    // ...
  }
}

I've been slowly adding more values to ValueRequirement, and I just added a new one, 'Another Widget\'s Value'. For this, I realize I can't actually check if this is valid or not unless validate is passed a list of all other Widgets that exist.

On one hand, I could just add allWidgets: Widget[] as an optional parameter and throw a runtime exception if it is undefined if and only if the ValueRequirement is 'Another Widget\'s Value'.

What I'd rather have is a compile time error: if ValueRequirement is 'Another Widget\'s Value', then a list of all other Widgets must be passed in as an additional parameter. Otherwise, it can't be passed in. I imagine the solution would look something like this, but I can't figure out a way to get it to compile:

type ValueRequirement =
  | 'None'
  | 'Any String'
  | 'Any Number'
  | 'Another Widget\'s Value';

type Widget = {
  value: string
}

type WidgetValidationRule = {
  widget: Widget,
  valueRequirement: ValueRequirement,
}

function isANumber(value: string): boolean {
  throw new Error("Function not implemented.");
}

function validate({widget, valueRequirement: 'Another Widget\'s Value'}: WidgetValidationRule, allWidgets: Widget[]): void;
function validate({widget, valueRequirement: 'None' | 'Any String' | 'Any Number'}: WidgetValidationRule): void;
function validate(widgetValidationRule: WidgetValidationRule, allWidgets?: Widget[]): void {
  //impl
}

There are a lot of similar questions about overloading in general, but in this case, I would have to destructure a type and know its ValueRequirement value to overload.

I've been reading the handbook on how to destructure objects and function overloading, but they don't mention this kind of use case. I'm not sure if that means it's not possible, I'm reading the wrong part of the docs, or this is a very unusual question that few have ever asked (I tried googling for this before writing this).

The title of Destructuring with function overloading seems similar, but it's about destructuring inside the implementation. I'm talking about destructuring the function signature itself.

In typescript, can you overload a function based on a destructured parameter?


I think I'm going to move ValueRequirement from WidgetValidationRule to the validate function as a parameter. Then this seems straightforward. Regardless, I'm curious about the answer to this question.


You can achieve this goal by making WidgetValidationRule generic (with a default parameter so that you can continue to use it as you do now), and then discriminating the parameter types in an overload based on the generic:

TS Playground

type ValueRequirement =
  | 'None'
  | 'Any String'
  | 'Any Number'
  | 'Another Widget\'s Value';

type Widget = {
  value: string
};

type WidgetValidationRule<T extends ValueRequirement = ValueRequirement> = {
  widget: Widget;
  valueRequirement: T;
};

function validate(widgetValidationRule: WidgetValidationRule<Exclude<ValueRequirement, 'Another Widget\'s Value'>>): void;
function validate(widgetValidationRule: WidgetValidationRule<'Another Widget\'s Value'>, otherWidgets: Widget[]): void;
function validate(widgetValidationRule: WidgetValidationRule, otherWidgets?: Widget[]): void {
  // implement
}

validate({
  widget: {value: 'one'},
  valueRequirement: 'None',
}); // ok

validate({
  widget: {value: 'one'},
  valueRequirement: `Another Widget's Value`,
});// Error (2322)

validate({
  widget: {value: 'one'},
  valueRequirement: `Another Widget's Value`,
}, [{value: 'hello'}, {value: 'world'}]); // ok

You might also split your union type into logical groups also to make it more readable:

TS Playground

type IndependentValueRequirement =
  | 'None'
  | 'Any String'
  | 'Any Number';

type DependentValueRequirement = 'Another Widget\'s Value';

type ValueRequirement = IndependentValueRequirement | DependentValueRequirement;

type Widget = {
  value: string
};

type WidgetValidationRule<T extends ValueRequirement = ValueRequirement> = {
  widget: Widget;
  valueRequirement: T;
};

function validate(widgetValidationRule: WidgetValidationRule<IndependentValueRequirement>): void;
function validate(widgetValidationRule: WidgetValidationRule<DependentValueRequirement>, otherWidgets: Widget[]): void;
function validate(widgetValidationRule: WidgetValidationRule, otherWidgets?: Widget[]): void {
  // implement
}