Проблема при использовании дистрибутивных условных типов в сочетании с универсальным методом

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

Я использовал https://www.typescriptlang.org/docs/handbook/advanced-types.html в качестве примера (раздел: Распределительные условные типы)

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

Это неуниверсальная версия:

export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;

interface Test {
  test: string;
}

function non_generic(form: Test, field: StringPropertyNames<Test>): string {
  return form[field];
}

Это работает.

Теперь, когда я изменяю интерфейс Test на общий аргумент, он больше не будет компилироваться.

export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;

function generic<T>(form: T, field: StringPropertyNames<T>): string {
  return form[field]; // This won't compile
}

Это ожидаемое поведение? Или это ошибка машинописи? Может кто-нибудь указать мне, как заставить работать универсальную версию (без каких-либо взломов)

Обновление 1:

Ошибка компиляции:

Type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.

Ссылка на игровую площадку

2 ответа

Решение

Компилятор, как правило, не может определить присваиваемость неразрешенных условных типов (то есть условных типов, которые не могут быть с нетерпением оценены, поскольку хотя бы один из T или же U в T extends U ? V : W еще не полностью указан). Это скорее ограничение дизайна, чем ошибка; компилятор не будет таким же умным, как человек (обратите внимание на себя: вернитесь сюда, когда произойдет восстание машины, и отредактируйте это), поэтому мы не должны ожидать, что он просто "заметит", что T[TypedPropertyName<T,P>] extends P всегда должно быть правдой. Мы могли бы написать конкретный эвристический алгоритм, чтобы обнаружить ситуацию и выполнить желаемое сокращение, но он должен был бы иметь возможность работать очень быстро, чтобы он не ухудшал время компиляции в 99% случаев, когда это не будет полезно.

Может кто-нибудь указать мне, как заставить работать универсальную версию (без каких-либо взломов)

Это действительно зависит от того, что вы считаете взломать. Абсолютно простейшая вещь - это использовать утверждение типа, которое явно предназначено для случаев, когда вы знаете, что что-то является безопасным типом, но компилятор не может это выяснить:

function generic<T>(form: T, field: StringPropertyNames<T>): string {
  return form[field] as any as string;  // I'm smarter than the compiler 
}

Или вы можете попытаться провести компилятор через шаги, необходимые для понимания того, что вы делаете безопасно. В частности, компилятор понимает, что Record<K, V>[K] присваивается V (где Record<K, V> определяется в стандартной библиотеке как сопоставленный тип, ключи которого находятся в K и чьи значения в V). Поэтому вы можете ограничить тип T как это:

function generic<T extends Record<StringPropertyNames<T>, string>>(
  form: T,
  field: StringPropertyNames<T>
): string {
  return form[field]; // okay
}

Теперь компилятор счастлив. И ограничение T extends Record<StringPropertyNames<T>, string> на самом деле не является ограничением, так как любой тип объекта будет соответствовать ему (например, {a: string, b: number} продолжается Record<'a', string>). Таким образом, вы должны иметь возможность использовать его везде, где вы используете исходное определение (для конкретных типов T тем не мение):

interface Foo {
  a: string;
  b: number;
  c: boolean;
  d: "d";
}
declare const foo: Foo;
generic(foo, "a"); // okay
generic(foo, "d"); // okay

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

Честно говоря, я не знаю, в чем проблема. Вы можете попробовать заполнить вопрос об их GH. Однако я знаю, что следующее работает без явного указания типа возвращаемого значения:

function generic<T>(form: T, field: StringPropertyNames<T>) {
  return form[field];
}

и он даже правильно вводит возвращаемое значение в виде строки:

const test = {
  a: "b",
  c: 1,
  "other": "blah"
}
generic(test, "a").charAt(0) //passes - "b"
generic(test, "a") * 5 // fails - function result is not a number
generic(test, "c") //fails - "c" is not assignable to "a" | "other"

Я бы дополнительно рекомендовал это дополнение, чтобы убедиться, что первый аргумент должен быть объектом:

function generic<T extends object>(form: T, field: StringPropertyNames<T>) {
  return form[field];
}
Другие вопросы по тегам