#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:
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)
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)?
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:
filterType
and isFilterable
);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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With