typescript - 如何更新深层嵌套对象(类型缩小没有按预期工作,keyof 编译错误)
我正在编写一个函数来更新深度嵌套的对象。目标是能够像这样输入 copyWith(person, 'address' ,'street', 'zip', 98443) 来生成一个副本,其中深层嵌套的属性与原始对象不同。我认为这是某些函数式库中的 lenses 功能。它有点可行。请看下面的测试。但是,如果我为输入对象添加类型定义——我喜欢这样做,因为它有助于确保我创建的是一个有效的源对象——那么它就无法编译,而且我不知道为什么。在我尝试调用 copyWith 的代码处,传递给 copyWith 的变量类型不应该被缩小到带有 custom 设置的 signedIn 路径吗?有没有办法让它在不显式创建和使用该缩小路径的类型的情况下工作?
it('can clone object with a deep change', () => {
const copyWith = <
T,
A extends keyof T,
B extends keyof T[A],
C extends keyof T[A][B],
V
>(
i: T,
a: A,
b: B,
c: C,
value: V
) => ({ ...i, [a]: { ...i[a], [b]: { ...i[a][b], [c]: value } } });
type Privacy = { mode: 'public' } | { mode: 'private' };
type Settings =
| { settings: 'default' }
| { settings: 'custom'; privacy: Privacy };
type User =
| { state: 'unauthorized' }
| { state: 'signedIn'; settings: Settings };
// =========================================================
// If add :User type definition, the test no longer compiles
// =========================================================
const user = {
state: 'signedIn',
settings: { settings: 'custom', privacy: { mode: 'private' } },
};
if (user.state === 'signedIn' && user.settings.settings === 'custom') {
const copied = copyWith(user, 'settings', 'privacy', 'mode', 'public');
assert.equal(copied.settings.privacy.mode, 'public');
} else {
throw new Error('This path should not have been taken.');
}
});
答案#1
你认为在这里应该使用判别联合是正确的。不幸的是,它对嵌套对象不起作用(希望将来能行)。你必须使用顶层联合来确定整个类型结构:
const copyWith = <
T,
A extends keyof T,
B extends keyof T[A],
C extends keyof T[A][B],
V
>(
i: T,
a: A,
b: B,
c: C,
value: V
) => ({ ...i, [a]: { ...i[a], [b]: { ...i[a][b], [c]: value } } });
type Privacy = { mode: 'public' | 'private' };
type User =
| { state: 'unauthorized' }
| {
state: 'signedIn';
settings: { settings: 'default' }
}
| {
state: 'signedIn';
settings: { settings: 'custom'; privacy: Privacy }
}
// =========================================================
// If add :User type definition, the test no longer compiles
// =========================================================
const user: User = {
state: 'signedIn',
settings: { settings: 'custom', privacy: { mode: 'private' } },
};
if (user.state === 'signedIn' && user.settings.settings === 'custom') {
const copied = copyWith(user, 'settings', 'privacy', 'mode', 'public');
assert.equal(copied.settings.privacy.mode, 'public');
} else {
throw new Error('This path should not have been taken.');
}
TS 游乐场
还有……隐私类型可以简单一点 ;)
