Ошибка типа контравариантности функционального поля интерфейса TypeScript

#typescript #interface #typeerror #covariance #contravariance

#typescript #интерфейс #ошибка типа #ковариация #контравариантность

Вопрос:

У меня есть следующий тип, который устанавливает, что все свойства будут функциями, которые не принимают либо никаких аргументов, либо один аргумент типа Record<string, any> :

 type FnTrait = Record<
  string,
  (input?: Record<string, any>) => any
>;
  

Я пытаюсь расширить этот тип на другой интерфейс (который я хочу иметь с тем же ограничением).

 interface HasFn extends FnTrait {
  someFn(): string; // no problem
  anotherFn(o: {id: string}): number; // ts error 2411
}
  

Это приводит к ошибке на anotherFn : Property 'anotherFn' of type '(o: { id: string; }) => number' is not assignable to string index type '(input?: Record<string | number | symbol, any> | undefined) => any'.

Почему это someFn не приводит к ошибке, в то время как anotherFn выдает ошибку ts 2411? Кажется, что это сужение должно быть разрешено.

Будем признательны за любую помощь. Спасибо!

Ответ №1:

Это пример того, что типы функций контравариантны по своим параметрам. Слово «контравариантный» означает «изменяется противоположным образом». Если вы делаете параметр функции более специфичным (узким), вы делаете сам тип функции более общим (широким). Это означает, что вместо того, чтобы создавать HasFn подтип FnTrait (что extends и означает), вы как бы делаете его супертипом. Это недопустимо и нарушает принцип взаимозаменяемости.

В частности, anotherFn() ошибка, потому что она требует, чтобы ее аргумент имел свойство с string значением id , в то время как FnTrait["anotherFn"] этого не делает. Ожидается, что вы можете вызвать любое свойство FnTrait либо без параметров, либо с одним параметром практически любого типа. Но HasFn может произойти сбой, если вы вызовете его anotherFn() метод без параметра или с параметром, в котором отсутствует правильное id свойство. Поэтому, как определено, HasFn не может быть присвоено FnTrait , несмотря на то, что оно объявлено для его расширения:

 const hasFn: HasFn = {
  someFn: () => "",
  anotherFn: o => o.id.length
}
const fnTrait: FnTrait = hasFn;
fnTrait.anotherFn({ a: 123 }); // okay at compile time, explodes at runtime
  

Поскольку anotherFn() означает, что вы не можете безопасно подставить FnTrait значение там, где запрашивается HasFn значение, FnTrait не может быть присвоено HasFn , и вы получаете ошибку.


Причина, по которой someFn() ошибка не заключается в том, что функция с меньшим количеством параметров может быть назначена функции, которая принимает больше параметров. Это связано с тем, что someFn() по необходимости будет игнорировать любые переданные в него параметры, поэтому безопасно рассматривать его как функцию, которая, возможно, может получить параметр:

 fnTrait.someFn({ a: 123 }); // okay at compile time and runtime
  

Это работает по той же причине, anotherFn() по которой происходит сбой: взаимозаменяемость.

Игровая площадка ссылка на код

Комментарии:

1. спасибо вам за ваше потрясающее описание! Еще один вопрос: каков самый минимальный способ позволить пользователям накладывать ограничение типа аргумента метода?… extends FnTrait чист. Какие-либо другие способы, которые вы можете порекомендовать, которые не приводят к ошибке контравариантности?