Search code examples
typescripttypescript-genericsreact-typescript

Issue with complex TypeScript union type


I have a function that filters some data from a server, based off of an array of options (an HTML select sort of thing). However, in some versions of the function I have all the data from the server from the get-go, and other times I only have part of the data. When I only have partial data, I want the filter's options array to be objects with content (what's rendered on the screen) and optionKey (which I send to the server for it to do the filtering). When I have the full data, I want the filter's options array to be objects with content, optionKey, and a filter function , that corresponds to a specific key of the filtered data. I scoured online and found a solution that works pretty well for me:

type HasPartialData = {
  options: ({ content: string; optionKey: string })[];
};

type HasFullData<T> = {
  [K in keyof T]-?: {
    columnKey: K;
    options: ({ content: string; optionKey: string; filter: (value: T[K], row: T) => boolean })[];
  };
}[keyof T];

type FilterProps<T> = (
  | ({
    hasPartial: true;
  } & HasPartialData)
  | ({
    hasPartial: false;
  } & HasFullData<T>)
);

However, I've run into a problem. I have a function (FilterMap) that receives a hasPartial flag and an array of either HasPartialData or HasFullData<T>, depending on the hasPartial flag:

type FilterMapProps<T> = (
  | {
    filters: HasPartialData[];
    hasPartial: true;
  }
  | {
    filters: HasFullData<T>[];
    hasPartial: false;
  }
);

But when I try to pass this to the Filter function, TypeScript can't tell that they're "synced up" and that passing them should work.

My original attempt was this:

function FilterMap<T>(props: FilterMapProps<T>) {
  return props.filters.map((filter) => {
    return Filter({ ...filter, hasPartial: props.hasPartial });
    //             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  });
}

But it causes this TypeScript error:

Argument of type '(HasFullData<T> & { hasPartial: boolean; }) | { hasPartial: boolean; options: { content: string; optionKey: string; }[]; }' is not assignable to parameter of type 'FilterProps<T>'.
    Type '{ hasPartial: boolean; options: { content: string; optionKey: string; }[]; }' is not assignable to type 'FilterProps<T>'.
      Type '{ hasPartial: boolean; options: { content: string; optionKey: string; }[]; }' is not assignable to type '{ hasPartial: true; } & HasPartialData'.
        Type '{ hasPartial: boolean; options: { content: string; optionKey: string; }[]; }' is not assignable to type '{ hasPartial: true; }'.
          Types of property 'hasPartial' are incompatible.
            Type 'boolean' is not assignable to type 'true'.(2345)

So I tried this:

  if (props.hasPartial) {
    return props.filters.map((filter) => {
      return Filter({ ...filter, hasPartial: props.hasPartial });
    });
  } else {
    return props.filters.map((filter) => {
      return Filter({ ...filter, hasPartial: props.hasPartial });
    });
  }

...which worked, but I think it's really ugly.

The only solution I felt kind of OK with was:

return props.filters.map((filter) => {
  return Filter({ ...filter as any, hasPartial: props.hasPartial });
});

But I don't like this use of as any...

Here's a full example in the TypeScript playground.

I really want a way to do this without the ugly if statement and without as any... Any help would be appreciated!


Solution

  • You could provide an extra generic parameter to capture the Partialness of each type:

    type HasData<T, Partial extends boolean> = (
      Partial extends true
        ? HasPartialData
        : HasFullData<T>
    )
    
    type FilterProps<T, Partial extends boolean> = (
        HasData<T, Partial> & { hasPartial: Partial }
    );
    
    type FilterMapProps<T, Partial extends boolean> = (
      { filters: HasData<T, Partial>[], hasPartial: Partial }
    );
    

    etc

    TS playground