Неправильный вывод типа аргумента функции высшего порядка - PullRequest
1 голос
/ 24 сентября 2019

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

Я могу легконапишите это:

function bar<T, P extends keyof T>(obj: T, p: P[]) {
  // use p to index obj somehow
  return obj;
}

bar({ a: 1, b: 'foo' }, ['a']); // Ok
bar({ a: 1, b: 'foo' }, ['a', 'b']); // Ok
bar({ a: 1, b: 'foo' }, ['a', 'b', 'c']); // Error: 'c' is not valid key

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

class Indexed {
  constructor(public a: number = 1) {}
  public app<P>(f: (obj: this, arg: P) => this, arg: P) {
    return f(this, arg);
  }
}

const a = new Indexed().app(bar, ['a']); // Error, `Type 'string' is not assignable to type '"a" | "app"'.`
const a = new Indexed().app(bar, ['wtf']); // The same

Если я использую bar напрямую, все работает как положено:

bar(new Indexed(), ['a']); // Ok
bar(new Indexed(), ['wtf']); // Err, as expected

Детская площадка

Вопрос:как написать app таким образом, чтобы он принимал / отклонял аргументы так же, как bar делает это?

Обратите внимание, что в целом я не знаю ограничений bar apriori, поэтому я не могу ограничить P с теми же границами, что и в bar.

1 Ответ

3 голосов
/ 24 сентября 2019

Я думаю, что это как раз тот случай, когда TypeScript расширяется от ["foo","bar"] до string[], потому что он не понимает, что вам нужен тип, чтобы оставаться кортежем строковых литералов ["foo", "bar"] (или, по крайней мере, массива строковых литералов Array<"foo"|"bar">).В вашей функции bar() ограничение P на keyof указывает на то, что компилятор не будет расширять строковые литералы до строки, но такой подсказки не существует для P в Indexed.app().

Либо вам нужно придумать способ изменить подпись Indexed.app(), чтобы намекнуть, что P следует выводить узким способом, где это возможно, без фактического ограничения (поскольку вы не знаете, что P будет, как вы сказали), или вам нужно найти способ намекнуть / указать, что P должно быть узким, когда вы звоните Indexed.app().


Изменение подписи app() для этого в настоящее время требует некоторых странных уловок, и до тех пор, пока не изменит , оно выглядит так:

type Narrowable =
  | string
  | number
  | boolean
  | symbol
  | object
  | undefined
  | void
  | null
  | {};

class Indexed {
  constructor(public a: number = 1) {}
  public app<
    N extends Narrowable,
    P extends N | [] | { [k: string]: N | P | [] }
  >(f: (obj: this, arg: P) => this, arg: P) {
    return f(this, arg);
  }
}

const a = new Indexed().app(bar, ["a"]); // okay
const b = new Indexed().app(bar, ["wtf"]); // error "wtf" not assignable to "a"|"app"

Подсказка на сайте вызова менее уродлива, если вызывающая сторона не забудет это сделать:

class Indexed {
  constructor(public a: number = 1) {}
  public app<P>(f: (obj: this, arg: P) => this, arg: P) {
    return f(this, arg);
  }
}
const a = new Indexed().app(bar, ["a" as "a"]); // okay
const b = new Indexed().app(bar, ["wtf" as "wtf"]); // error "wtf" not assignable to "a"|"app"

Или вы можете забыть подсказку и просто указать параметр типа самостоятельно:

const c = new Indexed().app<["a"]>(bar, ["a"]); // okay
const d = new Indexed().app<["wtf"]>(bar, ["wtf"]); // error "wtf" not assignable to "a"|"app"

Хорошо, надеюсь, одна из тех поможет.Удачи!

Ссылка на код

...