I want to define a function searchText
that takes an array of objects that needs to be filtered, array of object properties that will be searched through and a string value that will be searched for. In the end I want something like this to be possible:
type User = { firstName: string, lastName: string, age: number}
let users: User[] = [
{ firstName: 'John', lastName: 'Smith', age: 22},
{ firstName: 'Ted', lastName: 'Johnson', age: 32}
]
searchText(users, ['firstName', 'lastName'], 'john') // should find both entries
I want to constrain this function to only accept valid arrays of property names. A valid array should only contain properties that have a type of string since the function is searching for text. In another SO question I found a way to define type that should only allow valid values (https://stackoverflow.com/a/54520829/1242967). This type is defined as
type KeysMatching<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T];
With this type I am able to define a function that allows specifying the 'valid array constraint'
function searchTextInUsers(users: User[], stringFields: KeysMatching<User, string>[], text: string): User[]{
return users.filter(user =>
stringFields.some(field => user[field].toLowerCase().includes(text.toLowerCase()))
);
}
This compiles and works as expected. Trying to pass a non-string property causes a compilation error
searchTextInUsers(users, ['firstName', 'lastName', 'age'], 'john') // ERROR: Type 'string' is not assignable to type '"firstName" | "lastName"'.
But what I actually wanted to do is to write a function that would work for any type, not just User
but surprisingly just adding a generic type parameter did not work.
function searchText<T>(elements: T[], stringFields: KeysMatching<T, string>[], text: string): T[]{
return elements.filter(element =>
stringFields.some(field => element[field].toLowerCase().includes(text.toLowerCase())) // Error on this line
);
}
Compiler shows the following error
Property 'toLowerCase' does not exist on type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]'
Could someone explain why this did not work and why typescript does not narrow the type down to a string in this case? Is there another way to achieve what I want to do when using generic types? Playground link here