Search code examples
typescripttypesdiscriminated-union

Add an extra type property base on another property


#1 I have a type for the column that is an object. Column can be filterable or not, if isFilterable is true then the type Column should require: filterType, isTopBarFilter? and options (BUT only if filterType is 'SELECT' - #2).

type Column = {
  name: string;
  isFilterable: boolean; // passing here false should be equal with not passing the property at all (if possible)

  // below properties should exist in type only if isFilterable = true
  filterType: 'SELECT' | 'TEXT' | 'DATE';
  options: string[]; // this property should exist in type only if filterType = 'SELECT'
  isTopBarFilter?: boolean;
};

I do such type with use of types union and it work almost properly

type FilterableColumn = {
  isFilterable: true;
  filterType: 'SELECT' | 'TEXT' | 'DATE';
  options: string[];
  isTopBarFilter?: boolean;
};

type NonFilterableColumn = {
  isFilterable: false;
};

type Column = (NonFilterableColumn | FilterableColumn) & {
  name: string;
};

but:

  1. As I mentioned before (#2) Column should require options only if filterType is 'SELECT'. I have tried to do this with types union but it became works strange:
type FilterableSelectColumn = {
  filterType: 'SELECT';
  options: string[];
};

type FilterableNonSelectColumn = {
  filterType: 'TEXT' | 'DATE' | 'NUMBER';
};

type FilterableColumn = (FilterableSelectColumn | FilterableNonSelectColumn) & {
  isFilterable: true;
  isTopBarFilter?: boolean;
};

type NonFilterableColumn = {
  isFilterable: false;
};

type Column = (FilterableColumn | NonFilterableColumn) & {
  name: string;
};

// e.g
const col: Column = {
  name: 'col2',
  isFilterable: false,
  filterType: 'SELECT', // unwanted
  isTopBarFilter: false, // unwanted
  options: ['option1'], // unwanted
};

Playground

If I set isFilterable to false, TS doesn't suggesting unwanted properties (it is good) but also doesn't show error if I pass these unwanted props (it is bad)

  1. My solution also force to pass isFilterable even if it is false, as I mentioned above I want to pass it only if it is true

Is there way to improve my solution(or another solution) to achieve what I described at the beginning (#1)?


Solution

  • Ok, after a few nights I managed to do it, I have two solutions:

    1.

    type FilterableColumn = {
      isFilterable: true;
      isTopBarFilter?: boolean;
    } & (
      | {
          filterType: 'SELECT';
          options: string[];
        }
      | {
          filterType: 'TEXT' | 'DATE';
        });
    
    type NonFilterableColumn = {
      isFilterable?: undefined; // same result with never
      filterType?: undefined; // same result with never
    };
    
    type ColumnBaseFields = {
      name: string;
    };
    
    type Column = (FilterableColumn | NonFilterableColumn) & ColumnBaseFields;
    
    const column: Column = {
      name: 'someName',
      isFilterable: true,
      filterType: 'SELECT',
      options: ['option'],
    };
    

    Playground

    It works as I wanted, Typescript errors appears for the cases, but error descriptions are inaccurate. I noticed that TypeScript works strange with many unions on the same nesting level

    and because of it I made up the second solution with nested filters options

    2.

    type FilterSettings = (
      | {
          filterType: 'SELECT';
          options: string[];
        }
      | {
          filterType: 'TEXT';
        }) & {
      isTopBarFilter?: boolean;
    };
    
    type FilterableColumn = {
      isFilterable: true;
      filterSettings: FilterSettings;
    };
    
    type NonFilterableColumn = {
      isFilterable?: undefined; // same result with never
    };
    
    type ColumnBaseFields = {
      name: string;
    };
    
    type Column = (FilterableColumn | NonFilterableColumn) & ColumnBaseFields;
    
    const column: Column = {
      name: 'someName',
      isFilterable: true,
      filterSettings: {
        filterType: 'SELECT',
        options: ['option']
      }
    };
    

    Playground

    Works fine, typescript tell us exactly when some key is missed and when some key is unwanted.

    I hope it will be helpful for someone