Home > Software engineering >  How to determine which type of a function from a union is being used?
How to determine which type of a function from a union is being used?

Time:10-16

type TabWithSetIndex = (props: TabContentProps) => React.ReactElement;
type TabWithoutSetIndex = () => React.ReactElement;

interface ProjectCardProps {
  tabs: Array<{
    tabContent: TabWithSetIndex | TabWithoutSetIndex;
  }>;
}

Playground link

How to validate whether tabContent is of type TabWithSetIndex or TabWithoutSetIndex?

CodePudding user response:

A couple of options for you:

  • Function length with a type predicate
  • A discriminated union (with or without a type predicate)

Function length with a type predicate

You can do this with a type predicate like this:

function isTabWithoutSetIndex(
    tabContent: TabWithSetIndex | TabWithoutSetIndex
): tabContent is TabWithoutSetIndex {
    return tabContent.length === 0;
}

(The length property of a function is, roughly speaking, the number of declared formal parameters it has that don't have default values.)

Then, when you're looking at a specific tab.tabContent, you do:

if (isTabWithoutSetIndex(tab.tabContent)) {
    // It's `TabWithoutSetIndex`
    tab.tabContent();
} else {
    // It's `TabWithSetIndex`
    tab.tabContent({x: "foo"});
}

Playground link

That said, I worry about using the function's length, since this is a perfectly valid TabWithSetIndex that has a length of 0:

function thisIsATabWithSetIndex(...args[]) {
    // ...
}

...since rest parameters don't get count in length.

A discriminated union (with or without a type predicate)

To avoid using length, you can use a discriminated union, like this:

type TabWithSetIndex =
    ((props: TabContentProps) => React.ReactElement) &
    { __type__: "withSetIndex"; };
type TabWithoutSetIndex =
    (() => React.ReactElement) &
    { __type__: "withoutSetIndex" };

Then you'd have utility functions to set that __type__ property callers would pass their functions through:

function makeWithSetIndexFunction(
    fn: (props: TabContentProps) => React.ReactElement
): TabWithSetIndex {
    return Object.assign(fn, {__type__: "withSetIndex"}) as TabWithSetIndex;
}
function makeWithoutSetIndexFunction(
    fn: () => React.ReactElement
): TabWithoutSetIndex {
    return Object.assign(fn, {__type__: "withoutSetIndex"}) as TabWithoutSetIndex;
}

Then either just test __type__ directly:

if (tab.tabContent.__type__ === "withoutSetIndex") {
    // It's `TabWithoutSetIndex`
    tab.tabContent();
} else {
    // It's `TabWithSetIndex`
    tab.tabContent({x: "foo"});
}

...or use a type predicate that's more reliable now:

function isTabWithoutSetIndex(
    tabContent: TabWithSetIndex | TabWithoutSetIndex
): tabContent is TabWithoutSetIndex {
    return tabContent.__type__ === "withoutSetIndex";
}

Playground link

  • Related