简体   繁体   English

嵌套 TypeScript 函数中的泛型

[英]Generics in nested TypeScript function

I want to write a TypeScript function that accepts only certain strings, which are the property names of a specified type, but these properties must have a type of string , number or Date .我想编写一个只接受某些字符串的 TypeScript 函数,这些字符串是指定类型的属性名称,但这些属性的类型必须是stringnumberDate This function returns another function that accepts the original object (of which the property names were selected) and returns the specified property (actually it does something else, but this is the simplest case that produces the same problem)此函数返回另一个函数,该函数接受原始对象(其中选择了属性名称)并返回指定的属性(实际上它做了其他事情,但这是产生相同问题的最简单的情况)

I tried it like this我这样试过

"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);

TypeScript Playground打字稿游乐场

The problem is now that the type of res inside the nested function is something complex (instead of simply number | string | Date ), also in doSomethingEasy .现在的问题是嵌套函数内的res类型是复杂的(而不是简单的number | string | Date ),也在doSomethingEasy Is there any way to simplify the type of the property in the nested function to the primitive types?有什么方法可以将嵌套函数中的属性类型简化为原始类型?

Your doSomething() is a curried function where you don't know the type that T should be until you call the function returned by doSomething() .你的doSomething()是一个柯里化的函数,在你调用doSomething()返回的函数之前,你不知道T应该是什么类型。 This makes it nearly impossible for the compiler to infer T .这使得编译器几乎不可能推断T To make this obvious, imagine this:为了使这一点显而易见,想象一下:

const f = doSomething("asdf"); // what type should f be?
f({asdf: 123});
f({asdf: "a"});
f(test);

The parameter T gets inferred by the compiler when you call doSomething("asdf") to produce f .当您调用doSomething("asdf")以生成f时,编译器会推断参数T But what should it be inferred to be?但应该推断出它是什么? Should it depend on whatever you happen to call f() with?它应该取决于你碰巧用什么来调用f()吗? And what if you call f() with different types like I did above?如果你像我上面那样用不同的类型调用f()怎么办? The only thing that would work here is if f is itself a generic function, but the compiler does not know enough to try to infer that.唯一可以在这里工作的是,如果f本身是一个泛型函数,但编译器没有足够的知识来尝试推断它。

So you get T inferred as something like {asdf: any} (because reverse inference through conditional types like InstancePropertyNames is also essentially impossible), and then f() spits out any whatever you call it with.因此,您将T推断为类似{asdf: any} (因为通过条件类型(如InstancePropertyNames )进行反向推理基本上也是不可能的),然后f()吐出您调用它的any内容。 Oops.哎呀。


Instead what I'd do here is try to write your signature so that at each step the compiler only needs to know the type a function's parameters in order to infer its type parameters.相反,我在这里要做的是尝试编写您的签名,以便在每一步编译器只需要知道函数参数的类型即可推断其类型参数。 Something like this:像这样的东西:

function doSomething<K extends PropertyKey>(
    key: K
) {
    return function <T extends Record<K, string | number | Date>>(obj: T) {
        const res = obj[key];
        return res;
    }
}

Here doSomething will accept a key of type K which can be any key-like value.这里doSomething将接受K类型的key ,它可以是任何类似键的值。 Since key determines K , you can assume that K will be inferred properly.由于key决定K ,您可以假设K将被正确推断。 We don't make doSomething() generic in T since nothing passed to doSomething() will help determine what T should be.我们不会在T中使doSomething()泛型,因为没有传递给doSomething()将有助于确定T应该是什么。

The return value of doSomething() is another generic function which accepts an obj of type T which is constrained to Record<K, string | number | Date> doSomething()的返回值是另一个通用函数,它接受类型为Tobj ,该obj约束为Record<K, string | number | Date> Record<K, string | number | Date> Record<K, string | number | Date> . Record<K, string | number | Date> . So we pushed the T generic into the signature for the return function, which should actually know enough to infer it.所以我们将T泛型推入返回函数的签名中,它实际上应该知道足够的信息来推断它。

(This returned function doesn't have to be generic in T ; you could have type obj as Record<K, string | number | Date> directly, but then excess property checks kick in where you might not want them, and also the return value of the returned function would always be string | number | Date which is wider than you might want. Benefits of keeping T around below:) (这个返回的函数不必是通用在T ,你可以有键入obj作为Record<K, string | number | Date>直接,但随后多余的财产检查踢在你可能不希望他们,也回报返回函数的值将始终是string | number | Date ,它比您想要的更宽。将T保留在下面的好处:)

Since K is already known, this should mean that T will be inferred as the type of obj and it will only accept obj parameters whose value at the K key are assignable to string , number , or Date .由于K是已知的,这意味着T将被推断为obj的类型,并且它只接受obj参数,其K键的值可分配给stringnumberDate And the return type of the returned function will be T[K] , the type of the property of T at key K .并且返回函数的返回类型将为T[K] ,即T在键K处的属性类型。

Let's see if it works:让我们看看它是否有效:

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

Looks good.看起来挺好的。 The call to doSomething("asdf") returns a generic function accepting values assignable to {asdf: string | number | Date}doSomething("asdf")的调用返回一个通用函数,该函数接受可分配给{asdf: string | number | Date}{asdf: string | number | Date} {asdf: string | number | Date} {asdf: string | number | Date} . {asdf: string | number | Date} And so doSomething("asdf")(test) produces a value of type string .所以doSomething("asdf")(test)产生一个string类型的值。 The call at the end is shows the versatility of this implementation;最后的调用展示了这个实现的多功能性; you get a Date out because the compiler knows that the bar property of the passed-in object literal is a Date .你得到一个Date ,因为编译器知道传入的对象文字的bar属性是一个Date (That's why having both K and T be generic is useful. Otherwise Record<K, string | number | date>[K] is just string | number | date . The type T[K] is narrower and probably more helpful to you.) (这就是为什么KT都是通用的很有用。否则Record<K, string | number | date>[K]只是string | number | date 。类型T[K]更窄,可能对你更有帮助。 )

Playground link to code Playground 链接到代码

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

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