Upon dissecting the code example you provided, it is evident that you aim to filter the values
parameter against a filterString
while applying it to a specified propName
that must be present in a generic T
type, but with the condition that the value is an array.
There are a few key points that we need to address:
i) It is crucial to restrict T
to have a property that extends an Array
. This poses a challenge because TypeScript lacks a direct way to define an interface with at least one property of a specific type. However, we can implement a workaround to indirectly enforce this constraint.
ii) Moving on to the arguments, we need to ensure that the filterString
extends the generic type of the array being filtered. For example, if you intend to filter a propName
whose value is an Array<string>
, the filterString
argument should be of type string
.
iii) By constraining T
, we then define the argument propName
to be keys of T
with values that are arrays, in this case, of type string
.
While the implementation of your function definition remains the same, we could rearrange the arguments for the sake of clarity. Let's begin coding:
Firstly, we create a type that can extract the properties from T
that match a specific type. You can refer to @jcalz's solution for defining such an interface.
type Match<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
Now, we define the function based on the aforementioned steps:
// Defining types to filter keys with array values
type HasIncludes<T> = Match<T, { includes: (x: any) => boolean }>;
// Extracting the generic type of the array value
type IncludeType<T, K extends keyof T> = T[K] extends { includes: (value: infer U) => boolean } ? U : never;
// Applying the types to the function arguments
function customFilter<T, K extends HasIncludes<T>>(values: T[], propName: K, filterString: IncludeType<T, K>) {
// Casting to any as a workaround for TypeScript inference issue
return values.filter((value) => (value[propName] as any)
.includes(filterString))
}
The function is defined, and now we proceed to set up some test cases:
const a = [{ age: 4, surname: 'something' }];
const b = [{
person: [{ name: 'hello' }, { name: 'world' }]
}];
// In this scenario, there is an indirect error as any referred key does not meet the argument constraint
const c = [{ foo: 3 }]
// The generic type is implicitly inferred by its usage
const result = customFilter(b, 'person', { name: 'type' }); // Works as expected
const result = customFilter(a, 'age', 2); // TypeScript error due to 'age' not being an array
Feel free to experiment with the playground for further testing and exploration.