Looking at the example code you provided, you want to filter the values
argument against a filterString
of which you apply to a specified propName
that should exist in generic T
, but requires the value to be an array.
So we can highlight a few points that we should achieve:
i) We will need to constrain T
to have a property that extends an Array
. This will be a slight issue because ts
has no direct way of defining an interface with at least one property of a certain type. We will provide
a cheat though that will maintain an indirect constraint.
ii) So now we move to the args. Firstly, we want the filterString
to extend the generic type of the array you are filtering. For instance, say you want to filter a propName
whose value is an Array<string>
, so then we should expect filterString
argument to be of type string
right.
iii) With T
constrained we will then need to define the argument propName
to be keys of T
whose values are arrays. i.e. string
types.
The implementation of your function definition should stay the same, however we may rearrange the arguments for the order of definitions sake. So let us get coding:
We will first define a type that can pick out the properties from T
that are
of a specific type. please refer to @jcalz solution to defining such an interface.
type Match<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
So we define the function as described in the steps above
// we define these types that will filter keys whose values are arrays
type HasIncludes<T> = Match<T, { includes: (x: any) => boolean }>;
// and a type that will extract the generic type of the array value
type IncludeType<T, K extends keyof T> = T[K] extends { includes: (value: infer U) => boolean } ? U : never;
// we then apply then apply them to the args
function customFilter<T, K extends HasIncludes<T>>(values: T[], propName: K, filterString: IncludeType<T, K>) {
// so here we have to type cast to any as the first cheat because ts
// is unable to infer the types of T[K] which should be of type array
return values.filter((value) => (value[propName] as any)
.includes(filterString))
}
With the definition done we define some tests
const a = [{ age: 4, surname: 'something' }];
const b = [{
person: [{ name: 'hello' }, { name: 'world' }]
}];
// so in this case you get the indirect error discussed in that any key
// you refer will not meet the requirement of the argument constraint
const c = [{ foo: 3 }]
// note the generic is implicity inferred by its usage
const result = customFilter(b, 'person', { name: 'type' }); // works as expected
const result = customFilter(a, 'age', 2); // ts error because age is not array
Here is my playground so you can tinker in case you want to test specific cases.