Как гарантировать, что передаваемый ключ объекта может быть вызван с помощью Typescript? - PullRequest
0 голосов
/ 09 ноября 2019

Я пытаюсь создать компонент высшего порядка React для конкретного варианта использования, проблема сводится к следующему:

function sample<TObj, P extends keyof TObj, F extends keyof TObj>(
  obj: TObj,
  prop: P,
  setProp: TObj[F] extends (value: TObj[P]) => void ? F : never
) {
  obj[setProp](obj[prop]);
}

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

Это можно еще более упростить следующим образом:

function sample2<TObj, F extends keyof TObj>(
  obj: TObj,
  setProp: TObj[F] extends () => void ? F : never
) {
  obj[setProp]();
}

ItМне кажется, что, поскольку я использую условный тип, можно гарантировать, что obj[setProp] будет функцией, но я получаю ошибку: enter image description here

This expression is not callable.
  Type 'unknown' has no call signatures.ts(2349)

Как может бытькак видно ниже, функция выдаст ошибку, если она будет вызвана с ключом, который не соответствует требованию. Но это же требование, похоже, не применяется внутри функции.

Я понимаю, что это можно рассматривать как проблему XY, но меня действительно заинтересовало, есть ли способ решить эту конкретную проблемуработать правильно.

1 Ответ

1 голос
/ 09 ноября 2019

Внутри реализации sample2() тип TObj[F] extends () => void ? F : never является неразрешенным условным типом. То есть это условный тип, который зависит от неопределенного в данный момент параметра универсального типа, который необходимо разрешить. В таких случаях компилятор обычно не знает, что с ним делать, и рассматривает его как непрозрачный. (См. microsoft / TypeScript # 23132 для некоторого обсуждения этого.) В частности, он не понимает, что TObj[Tobj[F] extends ()=>void ? F : never] в конечном итоге придется разрешить в некоторый подтип ()=>void.

В общем, я бы полностью избегал условных типов, если они не нужны. Компилятор может легче понимать и выводить из сопоставленных типов , таких как Record<K, V>:

function sample2<K extends PropertyKey, T extends Record<K, () => void>>(
  obj: T,
  prop: K
) {
  obj[prop]();
}

И это ведет себя аналогично, когда вы вызываете его:

const obj2 = {
  func() { console.log("func") },
  prop: 42
};
sample2(obj2, "func"); // okay, 
//sample2(obj, "prop"); // error
//      ~~~ <-- number is not assignable to ()=>void

РЕДАКТИРОВАТЬ: чтобы обратиться к оригиналу sample(), я бы использовал это определение:

function sample<
  PK extends PropertyKey,
  FK extends PropertyKey,
  T extends Record<PK, any> & Record<FK, (v: T[PK]) => void>
>(
  comp: T,
  prop: PK,
  setProp: FK
) {
  comp[setProp](comp[prop]);
}

const obj = {
  func(z: number) { console.log("called with " + z) },
  prop: 42
}

, которое, я думаю, также ведет себя так, как вы хотели бы:

sample(obj, "prop", "func"); // called with 42
sample(obj, "prop", "prop"); // error!
//     ~~~ <-- number not assignable to (v: number)=>void
sample(obj, "func", "func"); // error!
//     ~~~ <-- (v: number)=>void not assignable to number

Хорошо, надеюсь, это поможет;удачи!

Ссылка на код

...