使用 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)
类型的对象只能保证 A
和 B
具有共同的键。但是 Type1
是没有共同键的对象的联合,因此 Type1
对象没有保证具有特定的键。
因此,无论 getFactory
上的类型注释如何,这都无法满足您的要求。您需要将 Type1
和 Type2
更改为交集类型。 (如果您需要其他东西的原始联合类型,请参阅 以了解从联合类型自动构造交集类型的方法。)
type Type1 = Field1 & Field2 & Field4
type Type2 = Field2 & Field3 & Field4
这是可行的,因为 keyof (A & B)
等同于 (keyof A) | (keyof B)
,因为 A & B
对象同时具有 A
和 B
的所有属性对象。
这修复了类型错误,但即使我们断言 { [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]
。
@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})
我正在尝试实现通用功能。这个函数的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)
类型的对象只能保证 A
和 B
具有共同的键。但是 Type1
是没有共同键的对象的联合,因此 Type1
对象没有保证具有特定的键。
因此,无论 getFactory
上的类型注释如何,这都无法满足您的要求。您需要将 Type1
和 Type2
更改为交集类型。 (如果您需要其他东西的原始联合类型,请参阅
type Type1 = Field1 & Field2 & Field4
type Type2 = Field2 & Field3 & Field4
这是可行的,因为 keyof (A & B)
等同于 (keyof A) | (keyof B)
,因为 A & B
对象同时具有 A
和 B
的所有属性对象。
这修复了类型错误,但即使我们断言 { [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]
。
@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})