Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)?

like image 723
bastej Avatar asked Nov 07 '22 01:11

bastej


1 Answers

Let's see how the distribution law affects how the intersections and unions resolve in your case. First, the following:

type OuterUnionMemberA = (UnionMemberA | UnionMemberB) & IntersectedA;

is equivalent to this:

type OuterUnionMemberA = (UnionMemberA & IntersectedA) | (UnionMemberB & IntersectedA);

which in turns leads us to the following:

type ComplexType = (OuterUnionMemberA | IntersectedB) & OuterIntersected;

being equivalent to this complex union:

type ComplexType = (UnionMemberA & IntersectedA & OuterIntersected) | (UnionMemberB & IntersectedA & OuterIntersected) | (IntersectedB & OuterIntersected);

Let's resolve the aliases manually and see what it leaves us with:

type ComplexType = { 
  filterType: 'SELECT'; 
  options: string[];
  isFilterable: true; 
  extraProp?: boolean;
  name: string;
} | {
  filterType: 'TEXT' | 'DATE' | 'NUMBER';
  isFilterable: true; 
  extraProp?: boolean;
  name: string;
} | {
  isFilterable: false;
  name: string
}

To verify our expectation of this being the same type, let's do an equality test:

type isSupertype = ComplexType extends ComplexTypeUnwrapped ? true : false; //true
type isSubtype = ComplexTypeUnwrapped extends ComplexType ? true : false; //true

All the above was done to make the following clear:

  1. There are 2 discriminant properties (filterType and isFilterable);
  2. In this case, the excess property check is not performed;

The combination of the above turns out to be a confirmed design limitation of TypeScript (see this and this issues on the source repository and the question that lead to the former issue being raised).

But what can you do about it? never to the rescue: being forbidden as a property is pretty much the same as having a type never, so a simple change to the isFilterable property accordingly to avoid making isFilterable a second discriminant property (extra optional property omitted for simplicity) should do the trick:

type Column = 
(
  { name: string, isFilterable:true,filterType:"SELECT",options:string[] } | 
  { name: string, isFilterable:true,filterType:"TEXT"|"DATE" } | 
  { name: string, isFilterable:never } |
  { name: string, isFilterable:false } //allows "name-only" case
)

const notFilterableAll: Column = { name: 'col2', isFilterable:false };
const notFilterableText: Column = { name: 'col2', filterType: "TEXT" }; //Property 'isFilterable' is missing;
const notFilterableSelect: Column = { name: "col2", filterType: "SELECT", options: [] }; //Property 'isFilterable' is missing;
const notFilterableSelectMissingOpts: Column = { name: "col2", filterType: "SELECT" }; //Type '"SELECT"' is not assignable to type '"TEXT" | "DATE"';
const selectFilterOk: Column = { name: 'col2', isFilterable:true, filterType: "SELECT", options: [] }; //OK
const textFilter: Column = { name: "col2", isFilterable:true, filterType: "TEXT" }; //OK

Playground

like image 120
Oleg Valter is with Ukraine Avatar answered Nov 11 '22 04:11

Oleg Valter is with Ukraine