Typescript: тип свойства объекта при индексации по суженному ключу универсального типа - PullRequest
0 голосов
/ 26 марта 2019

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

Например, рассмотрим простой случай числовых свойств:

type Adapter<T> = (from: T) => number

Я думаю, что хочу сделать что-то вроде:

type NumberFields<T> = {[K in keyof T]: T[K] extends number ? K : never}[keyof T]

function foo<T>(property: NumberFields<T>) {
    return function(from: T) {
        return from[property]
    }
}

TypeScript достаточно счастлив, когда аргумент NumberFields не является универсальным:

interface Foo {
    bar: number
}

function ok<T extends Foo, K extends NumberFields<Foo>>(property: K): Adapter<T> {
    return function(from: T): number {
        return from[property] // ok
    }
}

Однако в общем случае TypeScript не хочет распознавать, что from[property] может быть только числом:

function bar<T, K extends NumberFields<T>>(property: K): Adapter<T> {
    return function(from: T): number {
        return from[property] // error: Type 'T[K]' is not assignable to to type 'number'
    }
}

function baz<T, K extends NumberFields<T>>(property: K): Adapter<T> {
    return function(from: T): T[K] { // error: Type '(from: T) => T[K]' is not assignable to type 'Adapter<T>'
        return from[property]
    }
}

Почему это работает, когда тип ограничен, но не в действительно общем случае?

Я рассмотрел много вопросов SO, из которых Сужение типа индекса TypeScript показалось мне наиболее близким к тому, что я хочу, но здесь защита типа не нужна, поскольку тип должен быть универсальным.

1 Ответ

1 голос
/ 26 марта 2019

Компилятор просто не достаточно умен, чтобы видеть такие манипуляции с типами параметров универсального типа. С конкретными типами компилятор может просто оценить конечный тип как пакет конкретных свойств, поэтому он знает, что вывод будет иметь тип Foo['bar'], который имеет тип number.

Есть разные способы, чтобы продолжить здесь. Часто наиболее удобно использовать утверждение типа , потому что вы умнее компилятора. Это сохраняет исходящий JavaScript-код тем же, но просто говорит компилятору не беспокоиться:

function bar<T, K extends NumberFields<T>>(property: K): Adapter<T> {
  return function (from: T): number {
    return from[property] as unknown as number; // I'm smarter than the compiler ?
  }
}

Конечно, это не безопасно для типов, поэтому вам нужно быть осторожным, чтобы не лгать компилятору (например, from[property] as unknown as 12345 также будет компилятором, но, вероятно, это не так).

Эквивалентным утверждениям типа является функция single- overload , в которой вы задаете сигнатуру вызова настолько точную, насколько вам нужно, а затем ослабляете ее, чтобы реализация не жаловалась:

// callers see this beautiful thing
function bar<T, K extends NumberFields<T>>(property: K): Adapter<T>;

// implementer sees this ugly thing
function bar(property: keyof any): Adapter<any>
{
  return function (from: any): number {
    return from[property]; 
  }
}

Другой способ - попытаться позволить компилятору заверить вас в безопасности типов, перепечатав функцию в форме, понятной компилятору. Вот менее глубокий способ сделать это:

function bar<T extends Record<K, number>, K extends keyof any>(property: K): Adapter<T> {
  {
    return function (from: T): number {
      return from[property] 
    }
  }
}

В этом случае мы представляем T в терминах K, а не наоборот. И никаких ошибок здесь нет. Единственная причина, по которой не следует использовать этот метод, заключается в том, что, возможно, вы хотите, чтобы ошибки появлялись в K, а не в T. То есть, когда люди вызывают bar() с неверным параметром, вы хотите, чтобы он жаловался на неправильный тип K, а не на неправильный тип T.

Вы можете получить лучшее из обоих миров, выполнив следующее:

function bar<T, K extends NumberFields<T>>(property: K): Adapter<T>;
function bar<T extends Record<K, number>, K extends keyof any>(property: K): Adapter<T> {
  {
    return function (from: T): number {
      return from[property]
    }
  }
}

И последнее. Я не уверен, как вы планируете звонить bar(). Вы хотите сами указать параметры типа?

bar<Foo, "bar">("bar");

Или вы хотите, чтобы компилятор выводил их?

bar("bar")?

Если это последнее, вам, вероятно, не повезло, так как T выводить некуда, кроме, возможно, какого-то внешнего контекста. Так что вы можете захотеть посмотреть на это больше.


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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...