Проблема при использовании дистрибутивных условных типов в сочетании с универсальным методом
Я пытался сделать универсальную функцию, которая получает и объект 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];
}