[英]Generics in nested TypeScript function
我想编写一个只接受某些字符串的 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
。 有什么方法可以将嵌套函数中的属性类型简化为原始类型?
你的doSomething()
是一个柯里化的函数,在你调用doSomething()
返回的函数之前,你不知道T
应该是什么类型。 这使得编译器几乎不可能推断T
。 为了使这一点显而易见,想象一下:
const f = doSomething("asdf"); // what type should f be?
f({asdf: 123});
f({asdf: "a"});
f(test);
当您调用doSomething("asdf")
以生成f
时,编译器会推断参数T
。 但应该推断出它是什么? 它应该取决于你碰巧用什么来调用f()
吗? 如果你像我上面那样用不同的类型调用f()
怎么办? 唯一可以在这里工作的是,如果f
本身是一个泛型函数,但编译器没有足够的知识来尝试推断它。
因此,您将T
推断为类似{asdf: any}
(因为通过条件类型(如InstancePropertyNames
)进行反向推理基本上也是不可能的),然后f()
吐出您调用它的any
内容。 哎呀。
相反,我在这里要做的是尝试编写您的签名,以便在每一步编译器只需要知道函数参数的类型即可推断其类型参数。 像这样的东西:
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
将接受K
类型的key
,它可以是任何类似键的值。 由于key
决定K
,您可以假设K
将被正确推断。 我们不会在T
中使doSomething()
泛型,因为没有传递给doSomething()
将有助于确定T
应该是什么。
doSomething()
的返回值是另一个通用函数,它接受类型为T
的obj
,该obj
约束为Record<K, string | number | Date>
Record<K, string | number | Date>
Record<K, string | number | Date>
. 所以我们将T
泛型推入返回函数的签名中,它实际上应该知道足够的信息来推断它。
(这个返回的函数不必是通用在T
,你可以有键入obj
作为Record<K, string | number | Date>
直接,但随后多余的财产检查踢在你可能不希望他们,也回报返回函数的值将始终是string | number | Date
,它比您想要的更宽。将T
保留在下面的好处:)
由于K
是已知的,这意味着T
将被推断为obj
的类型,并且它只接受obj
参数,其K
键的值可分配给string
、 number
或Date
。 并且返回函数的返回类型将为T[K]
,即T
在键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}
值{asdf: string | number | Date}
{asdf: string | number | Date}
{asdf: string | number | Date}
。 所以doSomething("asdf")(test)
产生一个string
类型的值。 最后的调用展示了这个实现的多功能性; 你得到一个Date
,因为编译器知道传入的对象文字的bar
属性是一个Date
。 (这就是为什么K
和T
都是通用的很有用。否则Record<K, string | number | date>[K]
只是string | number | date
。类型T[K]
更窄,可能对你更有帮助。 )
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.