When structuring your code, it is essential to ensure that the filter
variable aligns with a union type that includes all properties of the Test
object. Here's an example:
type FilterItemTest = {
field: "name";
value: string;
operator: "startswith" | "doesnotstartwith" | "eq" | "neq";
} | {
field: "age";
value: number;
operator: "eq" | "neq";
}
By defining the types in this way, you can expect the desired behavior and receive errors for any incorrect assignments. For more information on potential issues, refer to microsoft/TypeScript#39438 and microsoft/TypeScript#40934.
let filter: FilterItemTest;
filter = { field: 'age', value: 10, operator: "eq" } // valid
filter = { field: 'age', value: 'test', operator: 'startswith' } // error, incompatible value
filter = { field: 'age', value: 10, operator: 'startswith' } // error, incompatible operator
filter = { field: 'name', value: 'test', operator: 'startswith' } // valid
In order for FilterItem<T>
to be versatile across different objects like Test
, incorporating generics is crucial. Here's one approach to achieve this:
To handle acceptable operators based on the property type, define Operator<T>
:
type Operator<T> =
(T extends string ? 'startswith' | 'doesnotstartwith' : never)
| 'eq' | 'neq'
This conditional type ensures the correct operators are assigned depending on the data type. Test this functionality as follows:
type OperatorString = Operator<string>;
// Result: "startswith" | "doesnotstartwith" | "eq" | "neq"
type OperatorNumber = Operator<number>;
// Result: "eq" | "neq"
Now, define FilterItem<T>
:
type FilterItem<T> = { [K in keyof T]-?: {
field: K;
value: T[K];
operator: Operator<T[K]>
} }[keyof T]
This distributive object type enables mapping over the properties of T
to generate union members effectively. It distributes the operation across all keys in keyof T
. Verify its correctness here:
type Test = {
name: string;
age: number;
}
type FilterItemTest = FilterItem<Test>;
/* Result:
{
field: "name";
value: string;
operator: Operator<string>;
} | {
field: "age";
value: number;
operator: Operator<number>;
}
*/
The dynamically created type FilterItem<Test>
mirrors the manual creation of FilterItemTest
, validating the successful completion of our task.
Explore code in TypeScript Playground