在对象迭代时如何断言匹配类型?



下面的代码块产生了一个打字错误,因为尽管我们知道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
});

这里有一个错误,因为forEachreduce是动态的。bar[k]number | string | booleanfoo[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所没有的属性。另外,仅仅因为foobar都有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)…

?

相关内容

  • 没有找到相关文章

最新更新