提问人:Michal Kurz 提问时间:6/14/2023 最后编辑:Michal Kurz 更新时间:6/14/2023 访问量:37
Currying 会破坏参数类型推断,因为参数列表被一分为二
Currying breaks argument type inference, because argument list gets split in two
问:
我有一个很好的功能,可以将对象变成我的选择选项:
type OptionValue = string;
type OptionLabel = string;
export type Option<V extends OptionValue = OptionValue, L extends OptionLabel = OptionLabel> = {
value: V;
label: L;
};
type ExtractStrings<T> = Extract<T, string>;
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKey: ValueKey,
labelKey: LabelKey,
objectIn: ObjectIn | null | undefined
): Option<string, string> | null => {
return null // The implementation is not important here
};
const myObj = { x: "foo", y: "bar" } as const
const result = toOption("x", (params) => `${params.x} (${params.y})`, myObj)
const result2 = toOption("x", "y", myObj)
这些类型工作得很好,我喜欢 TypeScript 为我强制执行这三个约束的方式:
ValueKey
必须存在于ObjectIn
LabelKey
如果是字符串,则必须存在于 上ObjectIn
- 如果 is 函数,则其参数被正确推断为
LabelKey
ObjectIn
我现在很想采用相同的函数,并整理最后一个参数 - 但是在输入它时遇到了明显的问题。如果我将所有泛型参数都保留在外部函数中,我将不再获得以下类型的推断:ObjectIn
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKey: ValueKey,
labelKey: LabelKey,
) => (objectIn: ObjectIn | null | undefined): Option<string, string> | null => {
return null // The implementation is not important here
};
const myObj = { x: "foo", y: "bar" } as const
const result = toOption("x", (params) => `${params.x} (${params.y})`)(myObj)
// Property 'y' does not exist on type 'Record<"x", string>' ^^^
而且我不能移动 ,因为这样当它被引用时就没有定义:ObjectIn
LabelKey
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
// ^^^
// Cannot find name 'ObjectIn'. Did you mean 'Object'?
>(
valueKey: ValueKey,
labelKey: LabelKey,
) => <
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(objectIn: ObjectIn | null | undefined): Option<string, string> | null => {
return null // The implementation is not important here
};
我是否可以做些什么来保持我之前指定的三个约束的执行,同时保持函数的库里?
我相信它不能像我的非咖喱例子那样通过直接推理来完成,因为外部函数是独立的,并且没有办法从以后可能或可能不会提供给其返回者的参数中推断出任何东西。但也许还有其他机制可以实现这一目标。
如果一个版本会迫使我在 时明确指定类型,同时在以下情况下仍然保留推理,我会很好:ObjectIn
LabelKey extends Function
LabelKey extends string
// Would throw error when `myObj` is not assignable to `ExplicitlySpecified`
const result = toOption("x", (p: ExplicitlySpecified) => `${p.x} (${p.y})`)(myObj)
const result2 = toOption("x", "y")(myObj)
通过将约束隔离到第三个函数(叹息)中,我有点接近了 - 但我不太喜欢第三个函数,现在我遇到了之前被定义的问题。ObjInConstrain
ValueKey
我有哪些选择?
感谢您抽出宝贵时间接受采访 🙂
答:
这可能会有所帮助 - 我认为您当前的方法可能正在尝试解决多个问题:咖喱(在您的情况下实际上是技术上的部分应用)和您的实现。
与其在你的方法中拥有咖喱逻辑,不如有明确定义的咖喱方法,为你做这件事。这里是操场。
首先,让我们定义一些咖喱类型和方法。
type FnCurry2<A,B,C> = (s1: A) => (s2: B) => C
type FnCurry3<A,B,C,D> = (s1: A) => (s2: B) => (s3: C) => D
export const curry2 = <A,B,C>(fn: (a: A, b: B) => C): FnCurry2<A,B,C> =>{
return (a: A) => (b: B) => fn(a, b);
}
export const curry2r = <A,B,C>(fn: (a: A, b: B) => C): FnCurry2<B,A,C> => {
return (b: B) => (a: A) => fn(a, b);
}
export const curry3 = <A,B,C,D>(fn: (a: A, b: B, c: C) => D): FnCurry3<A,B,C,D> =>{
return (a: A) => (b: B) => (c: C) => fn(a, b, c);
}
export const curry3r = <A,B,C,D>(fn: (a: A, b: B, c: C) => D): FnCurry3<C,A,B,D> => {
return (c: C) => (a: A) => (b: B) => fn(a, b, c);
}
现在让我们规范化您的方法以正常应用。
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKey: ValueKey,
labelKey: LabelKey,
objIn: ObjectIn | null | undefined
): Option<string, string> | null => {
return null // The implementation is not important here
};
在这一点上,让咖喱功能为您完成工作!
const toOptionCurried = curry3r(toOption);
const toOptionStringCat = toOptionCurried((params) => `${params.x} (${params.y})`)
const toOptionString = toOptionCurried("y")
const myObj = { x: "foo", y: "bar" } as const
const result = toOptionStringCat("x")(myObj);
const result2 = toOptionString("x")(myObj)
评论
const toOptionCurried = curry3r<"x", typeof myObj, "y" | ((params: typeof myObj) => string), Option<string, string> | null>(toOption);
你是对的,直接推理是不可能的(至少在遵循咖喱的约束下)。给出一些例子来说明为什么会这样,或者直接跳到答案。
请注意以下几点:
const myObj = { x: "foo", y: "bar" } as const
const result2 = toOption("x", "z")(myObj)
这里有一个错误!但它是 ,阅读如下:myObj
Argument of type '{ readonly x: "foo"; readonly y: "bar"; }'
is not assignable to parameter of type 'Record<"x" | "z", string>'.
但请注意错误是如何打开的!它不是不适用于 ,而是不适合 的对象形状。同样,给定以下函数myObj
z
myObj
myObj
{x: string, z: string}
const result = toOption("x", (params) => `${params.x} (${params.y})`)(myObj)
没有办法将 myObj 限制。如果有人做这样的事情怎么办?不运行咖喱功能?我们将如何验证参数?params
toOption("x", (params) => "")
答案
我提供了一些可能的解决方案,具体取决于您的用例/采用其中一种策略的意愿。
交换 curried 函数的顺序
我会接受一个版本,它会迫使我在以下情况下明确指定类型......
ObjectIn
如果是这样的话,那为什么不首先通过呢!我坚信,您的类型建模应该遵循您的数据。这意味着交换咖喱订单!像这样的东西......ObjectIn
export const toOption2 =
<
ObjectIn extends Record<string, OptionValue>
>(
objectIn: ObjectIn
) => (valueKey: keyof ObjectIn, labelKey: keyof ObjectIn | ((p: ObjectIn) => string)): Option<string, string> => {
return null!
};
const result1 = toOption2(foobar)("foo", (params) => `${params.foo} ${params.baz}`)
const result2 = toOption2(foobar)("foo", 'baz')
如果您仍然想保留旧形状,可以使用函数重载来支持这一点。您甚至可以支持关联咖喱。当然,实施会因此而变得复杂。虽然,在键入支持方面存在不同的限制。
function toOptionSolution<
ValueKey extends keyof ObjectIn,
LabelKey extends keyof ObjectIn,
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKeyOrObjIn: ObjectIn,
labelKey?: undefined
): (valueKey: ValueKey, labelKey: LabelKey | ((p: ObjectIn) => string) ) => void
function toOptionSolution<
ValueKey extends keyof ObjectIn,
LabelKey extends keyof ObjectIn,
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKeyOrObjIn: keyof ObjectIn,
labelKey: keyof ObjectIn | ((p: ObjectIn) => string | null)
): (objIn: ObjectIn) => string | null
function toOptionSolution
<
ValueKey extends string,
LabelKey extends string,
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKeyOrObjIn: ValueKey | ObjectIn,
labelKeyOrObjIn: LabelKey | ObjectIn,
): any
{
return null!
};
const foobar = {foo: 'bar', baz: 'bang'} as const
const notworking1 = toOptionSolution(foobar)("ababa", 'bababa')
const notworking2 = toOptionSolution(foobar)("foo", 'bababa')
const notworking3 = toOptionSolution('foo', 'bang')(foobar)
const working1 = toOptionSolution(foobar)("foo", (params) => `${params.foo} ${params.baz}`)
const working2 = toOptionSolution(foobar)("foo", 'baz')
创建咖喱类
这是一个略有不同的模式,灵感来自 Java 如何进行咖喱......或者 JS 示例就像 Jest 的样子
expect(sum(1, 2)).toBe(3);
因为一个类可以是泛型的,所以我们可以将该类型应用于该类,并且所有派生函数都可以使用该类型。这也很有用,因为它允许我们进行某种部分类型推断。因此,将类型传递给类以存储它,然后使用该方法进行咖喱。ObjectIn
ObjectIn
toOption
const foobar = {foo: 'bar', baz: 'bang'} as const
class ToOption<ObjectIn extends Record<string, string>> {
// Has no real properties...
constructor() {}
toOption =
<
ValueKey extends keyof ObjectIn,
LabelKey extends keyof ObjectIn | ((item: ObjectIn) => string)
>(
valueKey: ValueKey,
labelKey: LabelKey,
) => (objectIn: ObjectIn | null | undefined): string | null => {
return null!
}
}
const result = new ToOption<typeof foobar>().toOption('foo', (params) => `${params.foo} ${params.bang}`)(foobar)
const result2 = new ToOption<typeof foobar>().toOption('foo', 'bang')(foobar)
顺便说一句,如果你沿着这条路线完全转换为更像 OOP 的代码可能会更好。或者这里有一些替代建议:
const foobarc = ToOption<typeof foobar>()
// Multiple methods?
const result = foobarc.toOption('foo', 'baz').takeObject(foobar)
// Take in the key/labels in constructor?
const result2 = ToOption<typeof foobar>('foo', 'bar').takeObject(foobar)
您还可以让类扩展类以使其可调用。Function
const foobar = {foo: 'bar', baz: 'bang'} as const
// https://stackoverflow.com/a/40878674/17954209
class ExFunc extends Function {
[x: string]: any
constructor() {
super('...args', 'return this.__self__.__call__(...args)')
var self = this.bind(this)
this.__self__ = self
return self
}
}
interface ToOptionCompact<ObjectIn extends Record<string, string>> {
(objectIn: ObjectIn): string | null
}
class ToOptionCompact<ObjectIn extends Record<string, string>> extends ExFunc {
// Has no real properties...
constructor(valueKey: keyof ObjectIn, labelKey: keyof ObjectIn) {
super()
}
__call__ = (objectIn: ObjectIn | null | undefined): string | null => {
return null!
}
}
const result1 = new ToOptionCompact<typeof foobar>("foo", "baz")({} as {not: 'foobar'})
const result2 = new ToOptionCompact<typeof foobar>("foo", "baz")(foobar)
显式传递类型参数
最后的最糟糕选择。遗憾的是,这需要您指定所有三个参数,因为没有分部类型推理(请参阅建议:分部类型参数推理 #26242)。这是一个微不足道的练习,但这可能是最烦人的,因为它需要三个参数,以及一个难以内联注释的可能的函数类型参数。
评论
ObjIn
keyof
评论