【问题标题】:In TypeScript, how can I infer my arguments and impose a constraint that all items be keyof T, where T is the generic type for the function?在 TypeScript 中,我如何推断我的参数并强制所有项都是 keyof T,其中 T 是函数的泛型类型?
【发布时间】:2020-10-15 17:23:28
【问题描述】:

我的目标是创建一个名为:getFields 的函数。这个函数有一个通用的<T> 和一个参数...fields: Array<keyof T>。我希望这个函数返回一个函数,当给定<T> 类型的对象时,该函数将返回一个简化的对象,其中仅包含...fields 中命名的属性。

以下是一种辅助类型和我的getFields 实现:

type SubObj<T, S extends Array<keyof T>> = Pick<
  T,
  keyof { [K in S[number]]: K extends keyof T ? K : never }
>;

export function getFields<T extends Record<string, unknown>>(
  ...fields: Array<keyof T>
): (obj: T) => SubObj<T, typeof fields> {
  return (obj: T) =>
    Object.fromEntries(fields.map((field) => [field, obj[field]])) as SubObj<
      T,
      typeof fields
    >;
}

我用以下代码测试了这个实现:

type A = {
  a: string;
  b: string;
  c: string;
};

const b = getFields<A>('a', 'c')({ a: '', b: '', c: '' });

但是,当我查看typeof b 时,它是Pick&lt;A, "a" | "b" | "c"&gt;。我真正想要的是Pick&lt;A, "a" | "c"&gt;

我已经尝试了很多方法来使这项工作按我的预期进行,但唯一的成功是添加了第二个通用参数,这需要我将代码更改为:

const b = getFields<A, ['a','c']>('a', 'c')({ a: '', b: '', c: '' });

这对我来说太多余了,无法接受。

在这一点上,我认为我已经达到了我的 TypeScript 能力的极限,因为我想不出任何其他方法来完成我正在寻找的东西。

这甚至可以用 TypeScript 做吗?如果是这样,我需要做什么?

【问题讨论】:

    标签: typescript arguments constraints inference keyof


    【解决方案1】:

    TypeScript 目前不支持部分类型参数推断(请参阅microsoft/TypeScript#26242)。如果你在一个类型/函数中有多个类型参数,你要么需要明确地指定它们,要么让它们都被推断出来。没有办法指定一个并让另一个被推断出来。如果你想走这条路,有一些解决方法,但是这个问题会成为其他此类问题的重复,我可以指出,例如,this answer 以了解如何继续。

    不过,退一步说,这里的示例代码可能会以一种更简单的方式表示,根本不需要任何人指定任何类型。考虑一下:

    export function getFields<K extends PropertyKey>(
        ...fields: K[]
    ) {
        return <T extends Record<K, unknown>>(obj: T) =>
            Object.fromEntries(fields.map((field) => [field, obj[field]])) as Pick<T, K>;
    }
    

    在这里,我们从getFields() 的调用签名中完全删除了对象类型T。所有getFields() 关心的是获取一个类似键的参数列表。然后它返回一个也是通用的函数,并且this关心对象类型T,并将其限制为使用K中的键的东西。因此TK 都是可推断的:

    const b = getFields('a', 'c')({ a: '', b: '', c: '' }); 
    /* const b: Pick<{
        a: string;
        b: string;
        c: string;
    }, "a" | "c"> */
    

    由于getFields()的返回值是泛型函数,所以可以用于A以外的类型,例如:

    const c = getFields('a', 'c')({ a: 1, b: 2, c: 3 }) // {a: number, c: number}
    

    但如果你给它一些不恰当的东西,它仍然会出错:

    const d = getFields('a', 'c')({ a: 1 }) // error! property 'c' is missing
    

    如果你真的关心指定T,你可以在调用返回函数时这样做:

    const e = getFields('a', 'c')<A>({ a: 1, b: 2, c: 3 }); // error!  number is not string
    

    如果出于某种原因您真的想在开头指定T 并推断K,则需要使用其中一种解决方法进行部分类型推断,例如更多柯里化

    const stricterGetFields = <T,>() => <K extends keyof T>(...fields: K[]) => (obj: T) =>
        Object.fromEntries(fields.map((field) => [field, obj[field]])) as Pick<T, K>;
    
    const f = stricterGetFields<A>(); // specify here and do nothing else
    const g = f('a', 'c')({ a: "", b: "", c: "" }); // Pick<A, 'a'|'c'>; 
    

    Playground link to code

    【讨论】:

    • 虽然在开头指定T 会很好,但不这样做的简单性更可取。我感觉有一种更简单的方法可以解决这个问题。部分类型参数推断是我必须留意的。
    猜你喜欢
    • 2019-03-21
    • 1970-01-01
    • 2017-06-02
    • 2019-01-08
    • 2020-08-22
    • 1970-01-01
    • 2021-07-19
    • 2020-07-10
    • 2016-11-01
    相关资源
    最近更新 更多