下面的代码块产生了一个打字错误,因为尽管我们知道foo[k]
和bar[k]
是相同的类型,但TS不能知道(好吧,也许通过某种魔法它可以知道,但显然它没有)
interface IMixed {
a: number;
b: string;
c: boolean;
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k];
// ^^^^^^ Type 'string | number | boolean' is not assignable to type 'never'.
});
当TS不能找出我知道是真的东西时,我就把它放出来。但在这种情况下,虽然我知道这是一个有效的赋值,但我不知道它是哪种类型…我只知道它们是一样的。我想不出一个优雅的方法来使用泛型。
假设我致力于在这些属性上迭代的实践(比如,因为我们希望属性被添加到一行中,并且类似的代码存在于代码库中…)
简而言之:我如何断言这个赋值是有效的?
(附带问题:为什么错误将受让人称为"never"类型?)
@captain-yossarian对可变性提出了一个很好的观点,并提出了一个完全可行的不可变解决方案。我觉得这个问题仍然是开放的,但是,如果这个例子变得稍微复杂一点:
interface IMixed {
a: number[];
b: string[];
c: boolean[];
}
const foo: IMixed = { a: [1], b: ['one'], c: [true] };
const bar: IMixed = { a: [2], b: ['two'], c: [false] };
function intersection<T>(...arrays: T[][]): T[] {
return [...new Set([].concat(...arrays))]
}
(Object.keys(foo) as Array<keyof IMixed>)
.reduce((acc, elem) => ({
...acc,
[elem]: intersection(foo[elem], bar[elem])
// ^^^^^^^^^ Argument of type 'string[] | number[] | boolean[]' is not
// assignable to parameter of type 'string[]'.
}), foo);
关键是,赋值显然要求类型是兼容的…intersection
函数也是如此。所以这就像是原始问题的不可变版本。
在这种情况下值得使用reduce
而不是forEach
:
interface IMixed {
a: number;
b: string;
c: boolean;
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>)
.reduce((acc, elem) => ({
...acc,
[elem]: bar[elem]
}), foo);
游乐场
Mutations在TypeScript中不能很好地工作。
参见相关问题:第一,第二,第三,第四和我的文章
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k]; // error
});
这里有一个错误,因为forEach
和reduce
是动态的。bar[k]
是number | string | boolean
和foo[k]
的并集。这意味着在循环内部可以赋值foo['a'] = foo['c']
。这两个值都是有效的,因为我们期望number | string | boolean
的联合,但它也不安全。这就是为什么TS禁止这种行为。
另一方面,reduce
可以工作,因为我们创建了扩展IMixed
的新对象,而不是改变
为了能够改变foo
,你需要将indexing
添加到IMixed
接口:
type IMixed= {
a: number;
b: string;
c: boolean;
[prop: string]: number | boolean | string
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k];
});
游乐场
captain-yossarian有用地(一如既往地)指出,您不能依赖Object.keys(foo).forEach
来选择有效的密钥,因为它提供了foo
的所有密钥,但foo
可能是IMixed
的超类型,具有bar
所没有的属性。另外,仅仅因为foo
和bar
都有a
属性(例如)
似乎可以将循环建立在目标(foo
)的键上,并检查键是否存在于源(bar
)中,同时要求它们通过泛型参数共享公共子类型,如:
function copyAllProps<ObjectType>(target: ObjectType, source: ObjectType) {
// The `as` below is a bit of a lie :-D
(Object.keys(target) as Array<keyof ObjectType>).forEach(k => {
// NOTE: The type of `k` here is slightly wrong. We told TypeScript
// it's `keyof ObjectType`, but it could be something else, because
// `target` can have keys that `source` doesn't have (`target`'s type can
// be a supertype of `source`'s type). This `if` mitigates that by
// checking that `source` does have the property.
if (k in source) {
target[k] = source[k];
}
});
}
(注意其中的警告)它只复制通用属性。
用法示例:
// Your original examples
let foo: IMixed = { a: 1, b: 'one', c: true };
let bar: IMixed = { a: 2, b: 'two', c: false };
let bar2 = { a: 2, b: false, c: 'two'};
let foo3 = { a: 1, b: 'one', c: true, d: new Date() };
let bar3 = { a: 2, b: 'two', c: false, d: null };
function test() {
copyAllProps(foo, bar); // <== Works
copyAllProps(foo, bar2); // <== Error as desired, same keys but different types for `b` and `c`
copyAllProps(foo3, bar3); // <== Error as desired, same keys but different types for `d`
copyAllProps(foo, {}); // <== Error as desired, source isn't a match
}
操场上联系
这是一个解决方案的更新问题(不可变的版本)…从@captain-yossarian的回答和@AlekseyL中获得灵感。评论。
function intersection<K extends keyof IMixed>(k: K, mixed1: IMixed, mixed2: IMixed) {
return [...new Set([].concat(mixed1[k], mixed2[k]))]
}
(Object.keys(foo) as Array<keyof IMixed>)
.reduce((acc, elem) => {
return {
...acc,
[elem]: intersection(elem, foo, bar)
}
}, foo);
或…不如用更漂亮的东西来概括一下:
function pairwiseProcess<TIn, TOut>(
item1: TIn,
item2: TIn,
iteratee: <K extends keyof TIn>(k: K, item1: TIn, item2: TIn) => TOut
) {
return (Object.keys(item1) as Array<keyof TIn>).reduce((acc, key) => {
return {
...acc,
[key]: iteratee(key, item1, item2)
};
}, {});
}
console.log(pairwiseProcess(foo, bar, intersection));
对不起,试图把这个放在https://www.typescriptlang.org/play,但它显示了一个错误,我没有看到我的IDE(我甚至可以运行它与ts-node
)…