Ошибка допустимая; если у вас есть интерфейсы A
и B extends A
, то если я хочу A
, вы можете передать мне B
. Но с вашими определениями это ломается:
function callBaseProtocol(bp: BaseProtocol) {
return bp.action(12345);
}
function callAppProtocol(ap: AppProtocol) {
callBaseProtocol(ap); // oops!
}
Функция callBaseProtocol()
не ошибается. Определение BaseProtocol
говорит о том, что каждый строковый ключ - это функция, которая принимает значение типа unknown
и возвращает Promise<unknown>
. В частности, свойство action
для BaseProtocol
должно быть функцией, которая принимает значение типа unknown
. Поэтому вы можете передать его number
.
Функция callAppProtocol()
также не содержит ошибок. Здесь мы называем callBaseProtocol()
и передаем AppProtocol
. Это должно быть приемлемо, потому что AppProtocol extends BaseProtocol
.
Но если AppProtocol
имеет свойство action
, которое ожидает string
и вызывает некоторый метод string
-specifi c, например x => x.toUpperCase()
, это взорвется. Ошибка в том, что AppProtocol
, на самом деле, неправильно расширяет BaseProtocol
.
Это, возможно, удивительное явление известно как параметр контравариантности , где, если X
является подтипом Y
, то функция (y: Y)=>any
является подтипом (x: X)=>any
, и не наоборот . Направление подтипирования идет другим путем для параметров функции, и это означает "противоположность".
Это поведение было введено в TypeScript с помощью опции компилятора --strictFunctionTypes
и является улучшением безопасности типов.
Так как вы можете справиться с этим? Если вы хотите повысить безопасность типов и не снизить безопасность типов (вы упомянули, что не хотите использовать any
), то, возможно, ваш BaseProtocol
не совсем правильно определен. Возможно, вместо подписи строкового индекса и передачи unknowns
, это должен быть интерфейс generi c, который представляет более сложные отношения между вводом и выводом. Возможно, что-то вроде этого:
export type BaseProtocol<T> = {
[K in keyof T]: (payload: T[K]) => Promise<T[K]>;
}
Это говорит о том, что BaseProtocol
зависит от другого типа T
, и что BaseProtocol<T>
имеет метод для каждого свойства T
, который принимает параметр типа значения этого свойства и возвращает Promise
этого типа.
И тогда AppProtocol
может быть просто определен как:
export interface AppProtocol extends BaseProtocol<{ action: string }> {
}
Это решает проблему из ранее, так как компилятор не позволит вам вызвать метод BaseProtocol<T>
action()
для аргумента number
, если только он не знает, что T
имеет свойство action
типа number
:
function callBaseProtocol<T>(bp: BaseProtocol<T>) {
return bp.action(12345); // error! might not have an action
}
Существует также гибридный подход, который не предполагает, что возвращаемое значение каждой функции является обещанием того же типа, что и вход:
export type BaseProtocol<T> = {
[K in keyof T]: (payload: T[K]) => unknown;
}
export interface AppProtocol extends BaseProtocol<{ action: string }> {
action(x: string): Promise<string>;
}
Хорошо, надеюсь что помогает; удачи!
Детская площадка ссылка на код