繁体   English   中英

嵌套 TypeScript 函数中的泛型

[英]Generics in nested TypeScript function

我想编写一个只接受某些字符串的 TypeScript 函数,这些字符串是指定类型的属性名称,但这些属性的类型必须是stringnumberDate 此函数返回另一个函数,该函数接受原始对象(其中选择了属性名称)并返回指定的属性(实际上它做了其他事情,但这是产生相同问题的最简单的情况)

我这样试过

"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()的返回值是另一个通用函数,它接受类型为Tobj ,该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键的值可分配给stringnumberDate 并且返回函数的返回类型将为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 (这就是为什么KT都是通用的很有用。否则Record<K, string | number | date>[K]只是string | number | date 。类型T[K]更窄,可能对你更有帮助。 )

Playground 链接到代码

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM