我有一个以下函数,该函数采用扩展Foo(这是一个对象)的T类型的参数。在该函数中,它遍历给定对象的每个键,以创建一个具有完全相同的键,但对应的值均为1的新对象(此函数的作用并不重要)。
但是它无法使用Type '1' is not assignable to type 'T[Extract<keyof T, string>]'.
进行编译。我以为T[Extract<keyof T, string>]
是number
,所以分配1
的{{1}}应该可以。
我的代码有什么问题?
number
答案 0 :(得分:1)
编译器通常不会对泛型类型(即依赖于T
内的func()
之类的未解析类型参数的类型)进行非常复杂的操作分析...以更好地处理更简单的具体类型(例如Foo
)。
因此,编译器非常满意,并将允许使用以下函数的具体版本:
const concreteFunc = (obj: Foo): Foo => {
for (const name in obj) {
obj[name] = 1; // okay
}
return obj; // okay
};
由于尚不知道无法解析的泛型类型,因此编译器将不太确定您正在执行的操作是否安全,并且可能会发出警告。此警告不一定表示您肯定犯了一个错误。
这种情况通常在通用函数的实现内部发生。如果您仔细分析正在执行的操作并确定它确实是安全类型,则可以使用type assertions删除警告。
例如,您可以执行以下操作:
const func = <T extends Foo>(obj: T): T => {
for (const name in obj) {
obj[name] = 1 as T[typeof name]; // assert BUT BEWARE ☠
}
return obj;
};
但是请注意,类型断言意味着类型安全的责任已经从编译器转移到了您……并且(回答您的问题)这是不安全的。
这就是为什么...请考虑以下代码:
interface Bar extends Foo {
two: 2;
four: 4;
six: 6;
eight: 8;
}
const bar: Bar = {
two: 2,
four: 4,
six: 6,
eight: 8
};
const b = func(bar);
console.log(b.two); // 2 at compile time, but prints 1!
console.log(b.four); // 4 at compile time, but prints 1!
console.log(b.six); // 4 at compile time, but prints 1!
console.log(b.eight); // 4 at compile time, but prints 1!
在这里,我们看到接口Bar
通过添加值为numeric literals的已知属性扩展了Foo
,这些属性都不等于1
。当我们调用func(bar)
时,推断T
为Bar
,因此func(bar)
的输出也应该为Bar
。
坏事发生了。我们有一个对象,其已知属性在编译时应该是偶数,但实际上在运行时是数字1
。
这就是为什么您可能不应该在func()
之类的函数中使用断言的原因。可能有一种写func()
...的实际上安全的方法,就像这样:
const funcSafer = <
T extends { [K in keyof T]: 1 extends T[K] ? unknown : never }
>(
obj: T
): T => {
for (const name in obj) {
obj[name] = 1 // error! still need "as T[typeof name]"
}
return obj;
};
在这里,对T
的约束特别是1
应该可分配给它的所有属性。这具有以下理想效果:
funcSafer(bar); // error! property "two" is incompatible
const foo: Foo = {two: 2, four: 4}; // just Foo, not Bar
funcSafer(foo); // okay
funcSafer({a: 1 as 1}); // okay
funcSafer({a: 4}); // okay, interpreted as {a: number}
funcSafer({a: 4 as 4}); // error, "a" is incompatible
但是,当然,编译器仍然无法确定obj[name] = 1
在实现内部是安全的。太复杂了...所以我们需要断言。
好的,希望能有所帮助。祝你好运!