#typescript #typescript-generics
#typescript #typescript-обобщения
Вопрос:
Я хочу написать функцию TypeScript, которая принимает только определенные строки, которые являются именами свойств указанного типа, но эти свойства должны иметь тип string
, number
или Date
. Эта функция возвращает другую функцию, которая принимает исходный объект (для которого были выбраны имена свойств) и возвращает указанное свойство (на самом деле это делает что-то еще, но это простейший случай, который вызывает ту же проблему)
Я попробовал это так
"use strict";
type InstancePropertyNames<T, I> = {
[K in keyof T]: T[K] extends I ? K : never;
}[keyof T];
type StringPropertyNames<T> = InstancePropertyNames<T, string>;
type NumberPropertyNames<T> = InstancePropertyNames<T, number>;
function doSomething<T extends object>(
key:
| StringPropertyNames<T>
| NumberPropertyNames<T>
| InstancePropertyNames<T, Date>
) {
return function(obj: T) {
const res = obj[key];
return res;
}
}
function doSomethingEasy<T extends object>(
obj: T,
key:
| StringPropertyNames<T>
| NumberPropertyNames<T>
| InstancePropertyNames<T, Date>
) {
const res = obj[key];
return res;
}
type Test = {
asdf: string;
foo: number;
bar: boolean;
qux: Date;
};
const test: Test = {
asdf: "asdf",
foo: 42,
bar: true,
qux: new Date()
}
const res = doSomething<Test>("asdf")(test);
console.log(res);
const resEasy = doSomethingEasy(test, "asdf");
console.log(resEasy);
Теперь проблема в том, что тип res
внутри вложенной функции является чем-то сложным (вместо простого number | string | Date
), также в doSomethingEasy
. Есть ли какой-либо способ упростить тип свойства во вложенной функции до примитивных типов?
Ответ №1:
Ваша doSomething()
функция curried, в которой вы не знаете, какой тип T
должен быть, пока не вызовете функцию, возвращаемую doSomething()
. Это делает практически невозможным для компилятора вывод T
. Чтобы сделать это очевидным, представьте это:
const f = doSomething("asdf"); // what type should f be?
f({asdf: 123});
f({asdf: "a"});
f(test);
Параметр T
выводится компилятором при вызове doSomething("asdf")
функции produce f
. Но что это должно быть выведено? Должно ли это зависеть от того, что вы вызываете f()
с помощью? А что, если вы вызываете f()
с разными типами, как я сделал выше? Единственное, что здесь могло бы сработать, — это if f
сама по себе является универсальной функцией, но компилятор не знает достаточно, чтобы попытаться вывести это.
Таким образом, вы получаете T
вывод как что-то вроде {asdf: any}
(потому что обратный вывод через условные типы like InstancePropertyNames
также практически невозможен), а затем f()
выдает any
все, с чем вы это называете. Упс.
Вместо этого я бы попытался написать вашу подпись так, чтобы на каждом шаге компилятору нужно было знать только параметры функции типа a, чтобы вывести ее параметры типа. Что-то вроде этого:
function doSomething<K extends PropertyKey>(
key: K
) {
return function <T extends Record<K, string | number | Date>>(obj: T) {
const res = obj[key];
return res;
}
}
Здесь doSomething
будет принят key
тип K
, который может быть любым значением, подобным ключу. Поскольку key
определяет K
, вы можете предположить, что K
это будет выведено правильно. Мы не создаем doSomething()
generic, T
поскольку ничто, переданное в doSomething()
, не поможет определить, что T
должно быть.
Возвращаемое значение doSomething()
— это еще одна универсальная функция, которая принимает obj
тип T
of, который ограничен Record<K, string | number | Date>
. Итак, мы вставили T
generic в сигнатуру для возвращаемой функции, которая на самом деле должна знать достаточно, чтобы вывести ее.
(Эта возвращаемая функция не обязательно должна быть generic in T
; вы могли бы ввести type obj
as Record<K, string | number | Date>
напрямую, но тогда срабатывают избыточные проверки свойств там, где они могут вам не понадобиться, а также возвращаемое значение возвращаемой функции всегда string | number | Date
будет шире, чем вы могли бы хотеть. Преимущества сохранения T
ниже:)
Поскольку K
это уже известно, это должно означать, что T
это будет выведено как тип obj
, и он будет принимать только obj
параметры, значение которых в K
ключе можно присвоить string
, number
, или Date
. И возвращаемый тип возвращаемой функции будет T[K]
, тип свойства T
at key K
.
Давайте посмотрим, работает ли это:
const f = doSomething("asdf");
// const f: <T extends Record<"asdf", string | number | Date>>(obj: T) => T["asdf"]
const res = doSomething("asdf")(test); // string
console.log(res); // "asdf"
console.log(
doSomething("bar")({foo: true, bar: new Date(), baz: 123}).getFullYear()
); // 2020
Выглядит хорошо. Вызов doSomething("asdf")
возвращает обобщенную функцию, принимающую значения, которым можно присваивать {asdf: string | number | Date}
. И так doSomething("asdf")(test)
выдает значение типа string
. Вызов в конце показывает универсальность этой реализации; вы получаете a Date
out, потому что компилятор знает, что bar
свойство переданного объектного литерала равно a Date
. (Вот почему полезно иметь оба K
и T
быть универсальным. В противном Record<K, string | number | date>[K]
случае это просто string | number | date
. Тип T[K]
более узкий и, вероятно, более полезный для вас.)
Комментарии:
1. Спасибо за подробный ответ! Поскольку вы ответили так быстро, я не знаю, получили ли вы мою правку через 2 минуты после первоначального сообщения (она не видна). Я понял, что забыл передать тип в качестве параметра, для
doSomething
которого правильно разрешен возвращаемый тип, но в функции тип свойства довольно сложный, где он просто может бытьnumber | string | Date
. Можно ли это упростить? (кроме мошенничества сprop as unknown as string | number | Date
)2. А, понятно. Хорошо, я посмотрю, когда доберусь до реального компьютера, предполагая, что пока никто не ответит.
3.Хорошо, хорошо: нет, компилятор недостаточно умен, чтобы обобщенно рассуждать о том, что
T[IPN<T, string>] | T[IPN<T, number>] | T[IPNames<T, Date>]
будет присваиватьсяstring | number | Date
(не равно этому, потому что, например, тип{a: number}
должен выдаватьres
тип типаnumber
). Для конкретногоT
он может фактически оценить его, но не для неопределенного дженерикаT
. Если вы хотите, чтобы это произошло внутри реализации, вы должны использовать утверждение типа …. или вы могли бы использовать решение, которое я даю в этом ответе, которое понимает компилятор.4. Моим предлагаемым решением по-прежнему будет код в этом ответе, поскольку компилятору проще сделать вывод и обработать. Если по какой-то причине вам действительно нравится или нужно указывать
T
, когда вы вызываетеdoSomething()
, и сложные условныеInstancePropertyNames
ограничения, тогда вы получите что-то сложное, и вам нужно будет использовать утверждения типа или тому подобное, чтобы справиться с этим. Надеюсь, это поможет. Удачи!5. Спасибо за ваши предложения! Я пробовал это с утверждениями типа, но, например
res as number
, внутри функции выдает ошибку, что типыT[symbol]
иnumber
несовместимы… Я попробую это с вашим подходом дальше!