Определение типа для свойств объекта, которые являются строками. Различия между универсальными и явными типами

Ссылка на игровую площадку

Я хочу определить функцию searchText, которая принимает массив объектов, которые необходимо отфильтровать, массив свойств объекта, по которым будет выполняться поиск, и строковое значение, которое будет искать. В конце концов, я хочу, чтобы было возможно что-то подобное:

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

Я хочу ограничить эту функцию, чтобы она принимала только допустимые массивы имен свойств. Допустимый массив должен содержать только свойства, которые имеют тип строки, поскольку функция ищет текст. В другом вопросе SO я нашел способ определить тип, который должен допускать только допустимые значения (https://stackoverflow.com/a/54520829/1242967). Этот тип определяется как

type KeysMatching<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T];

С помощью этого типа я могу определить функцию, которая позволяет указать «допустимое ограничение массива».

function searchTextInUsers(users: User[], stringFields: KeysMatching<User, string>[], text: string): User[]{
    return users.filter(user =>
        stringFields.some(field => user[field].toLowerCase().includes(text.toLowerCase()))
    );
}

Это компилируется и работает, как ожидалось. Попытка передать свойство, не являющееся строкой, вызывает ошибку компиляции

searchTextInUsers(users, ['firstName', 'lastName', 'age'], 'john') // ERROR: Type 'string' is not assignable to type '"firstName" | "lastName"'.

Но на самом деле я хотел написать функцию, которая работала бы для любого типа, а не только User, но, что удивительно, простое добавление параметра универсального типа не сработало.

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
    );
}

Компилятор показывает следующую ошибку

Свойство toLowerCase не существует для типа T [{[K в ключе T]: T [K] расширяет строку? К: никогда; } [keyof T]] '

Может ли кто-нибудь объяснить, почему это не сработало и почему машинописный текст не сужает тип до строки в этом случае? Есть ли другой способ достичь того, что я хочу делать при использовании универсальных типов? Ссылка на игровую площадку


person user1242967    schedule 02.05.2020    source источник


Ответы (1)


Проще говоря, машинопись в данном случае не так умна, как вам хотелось бы.

В вашей searchTextInUsers функции он знает, что type K = KeysMatching<User, string> может быть только "firstName" | "lastName", и видит, что оба этих ключа возвращают свойства string из User. Но только по типу KeysMatching этого не понять.

Вместо того, чтобы работать в обратном направлении, используйте общий, который позволяет машинописному тексту двигаться вперед.

function searchText<K extends keyof any, T extends Record<K, string>>(elements: T[], stringFields: K[], text: string): T[]{
    return elements.filter(element => 
        stringFields.some(field => element[field].toLowerCase().includes(text.toLowerCase()))
    );
}

Здесь мы говорим, что тип K представляет ключи свойств строки, которые мы передаем как stringFields. Наши элементы должны иметь все эти ключи, и мы знаем, что соответствующие значения равны strings, поэтому мы говорим, что T должен расширять Record<K, string>.

Это работает, как ожидалось. searchText(users, ['firstName', 'lastName'], 'john') в порядке. Но если мы попытаемся передать имя свойства, которое не является строковым свойством, например 'age', мы получим ошибку, потому что массив данных users не выполняет контракт {age: string}.

searchText(users, ['firstName', 'age'], 'john')) выдает эту ошибку:

Argument of type '{ firstName: string; lastName: string; age: number; }[]' is not assignable to parameter of type 'Record<"firstName" | "age", string>[]'.
  Type '{ firstName: string; lastName: string; age: number; }' is not assignable to type 'Record<"firstName" | "age", string>'.
    Types of property 'age' are incompatible.
      Type 'number' is not assignable to type 'string'.

Ссылка на игровую площадку TS

person Linda Paiste    schedule 30.09.2020