场景
我是一位打字稿初学者,试图将下面的mapProps
函数移植到打字稿上
const addOne = x => x + 1
const upperCase = x => x.toUpperCase()
const obj = {
entry: 'legend',
fauna: {
unicorns: 10,
zombies: 3
},
other: {
hat: 2
}
}
const fns = {
entry: upperCase,
fauna: {
unicorns: addOne
},
other: obj => ({
hat: addOne(obj.hat)
})
}
const mapProps = (obj, fns) =>
Object.keys(fns).reduce(
(acc, cur) => ({
...acc,
[cur]: fns[cur] instanceof Function
? fns[cur](obj[cur])
: mapProps(obj[cur], fns[cur])
}),
obj
)
mapProps(obj, fns)
// { entry: 'LEGEND', fauna: { unicorns: 11, zombies: 3 }, other: { hat: 3 } }
当前尝试
type MapPropFuncsOf<T> = {
[P in keyof T]?: ((x:T[P]) => T[P]) | MapPropFuncsOf<T[P]>
}
const mapProps = <T>(obj:T, fns: MapPropFuncsOf<T>) =>
(Object.keys(fns) as (keyof T)[]).reduce(
(acc, cur) => ({
...acc,
[cur]: fns[cur] instanceof Function
? fns[cur]!(obj[cur]) // compiler warning
: mapProps(obj[cur], fns[cur]) // compiler warning
}),
obj
)
问题
即使我已经检查过fns[cur] instanceof Function
我似乎无法打电话给fns[cur]
,但打字稿编译器还是抱怨
无法调用类型缺少调用签名的表达式。类型 'MapPropFuncsOf | (((x:T [keyof T])=> T [keyof T])'没有 兼容的呼叫签名。
mapProps(obj[cur], fns[cur])
调用也失败了
“ MapPropFuncsOf”类型的参数| ((x:T [keyof T])=> T [keyof T])| undefined'不能分配给类型的参数 'MapPropFuncsOf'。类型“未定义”不可分配给 键入“ MapPropFuncsOf”。
答案 0 :(得分:1)
第一个问题是由打字稿不会缩小包含动态值的索引表达式的事实引起的。简单的解决方案是将值放入变量
declare var o: { [s: string]: string | number }
declare var n: string
if(typeof o[n] === "string") { o[n].toLowerCase(); } //error
let v = o[n]
if(typeof v === "string") { v.toLowerCase(); } // ok
因此,此代码在调用函数时不会引发错误:
const mapProps = <T>(obj:T, fns: MapPropFuncsOf<T>): T =>
(Object.keys(fns) as (keyof T)[]).reduce(
(acc, cur) =>
{
var fn = fns[cur];
return ({
...acc,
[cur]: typeof fn === "function"
? fn(obj[cur], null)
: mapProps(obj[cur], fns[cur])
})
},
obj
)
以上仍然不完美。 fn
用两个参数调用,这是不期望的。这实际上是由于您的代码不一致。毕竟,没有什么可以阻止T
具有函数属性。使用您的签名,您最终会调用可能不期望的功能。
我们可以限制T
不包含任何功能:
type MapPropFuncsOf<T extends Values> = {
[P in keyof T]?: ((x:T[P]) => T[P]) | (T[P] extends Values ? MapPropFuncsOf<T[P]>: T[P])
}
interface Values {
[s: string] : string | number | boolean | null | undefined | Values
}
const mapProps = <T extends Values>(obj:T, fns: MapPropFuncsOf<T>) =>
(Object.keys(fns) as (keyof T)[]).reduce(
(acc, cur) =>
{
var fn = fns[cur];
return ({
...acc,
[cur]: typeof fn === "function"
? fn(obj[cur]) // fully checked now
: mapProps(obj[cur], fns[cur])
})
},
obj
)
有关调用mapProps
的问题源于类似的问题,但不容易解决。
由于我们添加了T extends Values
约束,因此我们只能使用满足该约束的东西来调用mapProps
。我们可以将obj[cur]
放在一个变量中,例如o
,但这不能帮助我们使用Values
类型防护将值缩小到typeof o == "object"
。我们可以使用类型声明或自定义类型防护来摆脱上一期问题。
另外,对于原始类型递归调用mapProps
可能没有意义,因此我将使用仅检查object
的自定义类型防护。
type MapPropFuncsOf<T extends Values> = {
[P in keyof T]?: ((x:T[P]) => T[P]) | (T[P] extends Values ? MapPropFuncsOf<T[P]>: T[P])
}
interface Values {
[s: string] : string | number | boolean | Values
}
function isValues(o: Values[string]) : o is Values {
return typeof o === "object"
}
const mapProps = <T extends Values>(obj:T, fns: MapPropFuncsOf<T>) : T=>
(Object.keys(fns) as (keyof T)[]).reduce(
(acc, cur) =>
{
var fn = fns[cur];
var o = obj[cur];
return ({
...acc,
[cur]: typeof fn === "function"
? fn(obj[cur])
: (isValues(o) ? mapProps(o, fn as any) : o)
})
},
obj
)
最后一点是,fn
对于MapPropFuncsOf
而言不是有效的o
,因此我使用了fn as any
,恐怕这就是袋子的花招用完了。
注意
尽管尝试编写函数的完全类型化的版本很有趣,但是打字稿类型系统可以建模的方式受到限制。尤其是对于通用功能,对客户端代码进行完全类型检查更为重要。如果在实现中需要稍微突破类型安全沙箱,只要我们对此有明确的表述并有充分的理由这样做就不会是一件可怕的事情。类型声明版本已经在另一个答案中,因此在这里我不再重复。
答案 1 :(得分:0)
让我们从头开始。您的修饰符缺少类型。让我们添加它们。
const addOne = (x: number) => x + 1;
const upperCase = (x: string) => x.toUpperCase();
这两个函数均符合 identity函数的签名-它们接受某种类型的参数并返回完全相同的类型。
type Identity<T> = (argument: T) => T;
您的fns
是这些函数的集合。完成后,obj
的类型将不受影响。但是,它并不是那么简单-有时恒等函数适用于原始值,有时适用于对象的子树。如果我们要模拟obj
和fns
之间的关系,那将是这样的:
type DeepIdentity<T> = {
[K in keyof T]?: T[K] extends object
? DeepIdentity<T[K]> | Identity<T[K]>
: Identity<T[K]>
}
让我们看看我们的定义是否正确。
const fns: DeepIdentity<typeof obj> = {
entry: upperCase,
fauna: {
unicorns: addOne,
},
other: obj => ({
hat: addOne(obj.hat)
})
}
有了这些,您的处理功能可以定义为:
const process = <T>(input: T, transformers: DeepIdentity<T>): T =>
(Object.keys(input) as (keyof T)[])
.reduce(
(accumulator, property) => {
const transformer = transformers[property];
const value = input[property];
return Object.assign(accumulator, {
[property]: (transformer === undefined)
? value
: (transformer instanceof Function)
? transformer(value)
: process(value, transformer as DeepIdentity<typeof value>)
});
},
{} as T
)
process(obj, fns); // { "entry": "LEGEND", "fauna": { "unicorns": 11, "zombies": 3 }, "other": { "hat": 3 } }