使用 Typescript 实现通用柯里化函数

Implementing of generic curried function using Typescript

我正在尝试实现通用功能。这个函数的JS模拟是:

const getFactory = (field) => (value) => ({ [field]: value })

用例:

// types
type Field1 = { field1: string }
type Field2 = { field2: boolean }
type Field3 = { field3: number }
type Field4 = { field4: boolean }
type Type1 = Field1 | Field2 | Field4
type Type2 = Field2 | Field3 | Field4

// use case
const field1Factory        = getFactory<Type1>('field1')
const field2Factory        = getFactory<Type1>('field2')
const field4Factory        = getFactory<Type1>('field4')

const anotherField2Factory = getFactory<Type2>('field2')
const field3Factory        = getFactory<Type2>('field3')
const anotherField4Factory = getFactory<Type2>('field4')

const typeField1        = field1Factory('test')       // { field1: 'test' }
const typeField2        = field2Factory(true)         // { field2: true }
const typeField4        = field4Factory(false)        // { field4: false }
const typeAnotherField2 = anotherField2Factory(false) // { field2: false }
const typeField3        = field3Factory(3)            // { field3: 3 }
const typeAnotherField4 = anotherField4Factory(true)  // { field4: true }

我想它可能看起来像这样:

const getFactory = <Type extends {}>(field: keyof Type) => (value: Type[keyof Type]) => ({ [field]: value })

但是当我尝试获取工厂 (const field1Factory = getFactory('field1')) 时出现错误:

Argument of type '"field1"' is not assignable to parameter of type 'never'.(2345)

问题是 keyof 不分布在联合类型上。您可以通过检查 keyof Type1:

来验证这一点
type KeyOfType1 = keyof Type1;
// KeyOfType1 = never

发生这种情况是因为 (A | B) 类型的对象只能保证 AB 具有共同的键。但是 Type1 是没有共同键的对象的联合,因此 Type1 对象没有保证具有特定的键。

因此,无论 getFactory 上的类型注释如何,这都无法满足您的要求。您需要将 Type1Type2 更改为交集类型。 (如果您需要其他东西的原始联合类型,请参阅 以了解从联合类型自动构造交集类型的方法。)

type Type1 = Field1 & Field2 & Field4
type Type2 = Field2 & Field3 & Field4

这是可行的,因为 keyof (A & B) 等同于 (keyof A) | (keyof B),因为 A & B 对象同时具有 AB 的所有属性对象。

这修复了类型错误,但即使我们断言 { [field]: value }getFactory 定义中可能的最具体类型,也不会像您想要的那样具体地推断出结果类型:

const getFactory =
    <T>(field: keyof T) =>
        (value: T[keyof T]) =>
            ({ [field]: value } as { [K in keyof T]: T[K] })

const field1Factory = getFactory<Type1>('field1')

const typeField1 = field1Factory('test')

/* typeof typeField1 = {
    field1: string;
    field2: boolean;
    field4: boolean;
} */

这是因为 field1Factory 是用 T = Type1 调用的,所以它的属性是 keyof Type1,其中包括所有三个字段名称。为了做得更好,您需要另一个类型参数 K extends keyof T 作为字段名称,以推断正在使用 T 的哪个特定键。您还需要更具体的文字类型 'test' 作为值,因此您需要第三个类型参数 V extends T[K]

不幸的是,当一个函数有多个类型参数时,您不能指定一个而让编译器推断其他的。要么你必须像 getFactory<Type1, 'field1'>('field1') 那样写两次字符串文字,要么你可以添加另一层柯里化:

const getFactory2 =
    <T>() =>
        <K extends keyof T>(field: K) =>
            <V extends T[K]>(value: V) =>
                ({ [field]: value } as { [k in K]: V })

const field1Factory = getFactory2<Type1>()('field1')

const typeField1 = field1Factory('test')

/* typeof typeField1 = {
    field1: 'test';
} */

注意 getFactory2<Type1>()('field1') 中的额外 ()。但至少它可以推断出正确的、最具体的类型,而无需写两次字段名称。

类型断言 ... as { [k in K]: V } 是必要的,因为即使 field 具有更具体的类型 K{ [field]: ... } 也会被推断为 [x: string]

Playground Link

@kaya3 对所涉及的主题提供了很好的答案和解释,但是您可以继续使用联合类型而不是交集,通过使用条件类型分布在联合上以提取键 (ExtractKeys) 和另一种条件类型 (ExtractValue) 将值类型的解析推迟到调用时间启用以捕获文字(例如 true 而不是 boolean)。如果不需要,可以使用 T[K] 代替。

type ExtractKeys<T> = T extends {} ? keyof T : never
type ExtractValue<T, K extends string | number | symbol> =
    T extends { [key in K]: infer U } ? U : never

const getFactory =
    <T>() =>
        <K extends ExtractKeys<T>>(field: K) =>
            <V extends ExtractValue<T, K>>(value: V) =>
                ({ [field]: value } as {[k in K]: V})