for-in 循环将 TypeScript 类型转换为交叉类型?
编辑:更新后的最小示例(希望如此):
归结起来,我想我好奇的核心是为什么这行不通,并且会导致下面的两个错误:
interface Implicit {
someNumber?: number | { value: number }
someString?: string | { value: string }
}
interface Explicit {
someNumber?: { value: number }
someString?: { value: string }
}
const impl:Implicit = {
someNumber: 123,
someString: { value: 'impl' }
}
function convert (obj: Implicit) {
const expl:Explicit = {}
let k: keyof typeof obj;
for (k in obj) {
const objk = obj[k] // also, why does this alter behavior?
if (typeof objk === 'object') {
expl[k] = objk // Error {value:string}|{value:number} not assignable to {value:string}&{value:number}
} else {
expl[k] = { value: objk } // Error string|number is not assignable to never
}
}
}
TS不能在每次循环中确定k是someNumber还是someString,所以它创建了一个交集,强制结果同时是两者吗?
更新的 TS 游乐场
上一个问题:
我正在尝试创建一个通用函数,该函数可以接受简写的隐式表示法 { someParameter: 123 } 或显式版本:{ someParameter: { value: 123, min: 0, max: 200 }},并转换后返回显式版本:
interface ImplicitValues {
color: number | string | ExplicitValueColor;
position: number | ExplicitValuePosition;
}
interface ExplicitOptions {
min?: number
max?: number
}
interface ExplicitValueColor extends ExplicitOptions {
value: number | string
}
interface ExplicitValuePosition extends ExplicitOptions{
value: number,
}
还有其他隐式值类型(为了简洁未列出),所以我想创建一个通用类型,能够自动转换为显式类型:
type MakeExplicit<T> = {
[K in keyof T]?: Extract<T[K], { value: any }>
}
这似乎有效,因为当我们使用鼠标悬停在使用 @jcalz 出色的 Expand 工具的类型上时,它是正确的:
然而,当我们尝试遍历 ImplicitValues 数组,并将它们显式化时,我们会得到以下错误:
const someObject:ImplicitValues = {
color: 0xff0000,
position: {
value: 123,
min: 0,
max: 200
}
}
function isExplicit <T>(param: any): param is MakeExplicit<T> {
if (typeof param === 'object') return 'value' in param;
return false;
}
function convertToExplicit(obj:ImplicitValues) {
let explicitValues:MakeExplicit<ImplicitValues> = {};
let key:keyof typeof obj
for (key in obj) {
if (isExplicit(obj[key])) {
explicitValues[key] = obj[key]
// ERROR: Type 'string | number | ExplicitValueColor | ExplicitValuePosition' is
// not assignable to type 'ExplicitValueColor & ExplicitValuePosition'.
} else {
explicitValues[key] = { value: obj[key] }
// ERROR: Type 'string | number | ExplicitValueColor | ExplicitValuePosition' is
// not assignable to type 'number'.
}
}
return explicitValues
}
手动设置这些值可以正常工作,但在循环中动态设置就不行。我是应该直接强制转换 explicitValues[key]=(obj as any)[key] 然后了事,还是有更好的方法来做这件事?
如果有帮助的话,链接到 TS Playground。
答案#1
这里的主要问题是,由于 k 是联合类型,因此编译器也将 obj[k] 看作是联合类型:
const objk = obj[k];
/* string | number | { value: number; } | { value: string; } */
即使你从中消除了原始类型,obj[k] 仍然是联合类型:
if (typeof objk === "object") {
objk; /* { value: number } | { value: string } */
}
所以编译器所知道的只是你有一个类型为 {value: number} | {value: string} 的值 objk 和一个类型为 keyof Implicit 的键 k,而你正试图赋值 expl[k]=objk,其中 expl 的类型为 Explicit。通常来说,这是不安全的。就编译器所看到的情况而言,k 有可能是 "someNumber",而此时 objk 的类型却是 {value: string}:
expl[k] = objk; // error!
// Type '{ value: number; }' is not assignable to type '{ value: string; }'
(如果你想知道为什么错误消息涉及交叉类型,请参见 microsoft/TypeScript#30769。如果你有一个类型为 T 的对象 t 和一个类型为 K1 | K2 的(非泛型)键 k,并且你想给 t[k] 赋值,那么唯一安全的赋值就是适用于 T[K1] 和 T[K2] 的值,即 T[K1] & T[K2]。当你读取 t[k] 时,你得到的是 T[K1] | T[K2],但是当你写入 t[k] 时,你需要 T[K1] & T[K2]。)
我们知道这是不可能的,因为我们知道 k 和 objk 是相关的,但类型系统无法追踪这一点。因此会出现错误。对于相关联合类型的缺乏支持,是 microsoft/TypeScript#30581 讨论的主题。
处理这个问题最简单的方法就是使用类型断言。当你比编译器更了解某个表达式的类型时,你可以直接告诉编译器这个信息。或者你也可以直接告诉编译器不要担心,使用 any 断言:
expl[k] = obj[k] as any; // I know what I'm doing
没问题,但这会将部分类型安全的责任从编译器转移到你自己身上。如果你写错了东西,编译器是不会发现的:
expl[k] = obj["someNumber"] as any; // oops
如果你想从编译器获得更多类型安全保证,你需要使用泛型而不是联合类型,特别是你需要将你的 Explicit 和 Implicit 类型重构为泛型,并依赖一个公共接口,这样赋值 expl[k]=objk 就会被视为一个赋值,其中两边在该接口和泛型键 K 的意义下是相同的。
该方法在 microsoft/TypeScript#47109 中进行了描述,这是对 microsoft/TypeScript#30581 的修复(有趣的是,这里的重构在没有该拉取请求中的更改的情况下也能工作,因为大多数机制已经是语言的一部分)。
如果你有一个类型为 T 的对象 t,一个 泛型 键类型为 K 的键 k,以及一个类型为 T[K](也将是泛型)的值 v,那么编译器 会 允许你写 t[k]=v,即使如果 K 是非泛型联合类型同样的赋值会失败。(由于相同的原因,这实际上是不安全的,但编译器允许它。只要我们小心不要将 K 指定为联合类型,就没有问题。)
它看起来是这样的:
interface Base {
someNumber: number;
someString: string;
}
type Explicit<K extends keyof Base = keyof Base> =
{ [P in K]?: { value: Base[P] } };
type Implicit<K extends keyof Base = keyof Base> =
{ [P in K]: Base[P] | { value: Base[P] } };
Explicit 和 Implicit 类型在没有类型参数时的评估结果与原始版本相同,但现在你可以为某个特定的键写 Explicit<K>,编译器将知道它在该键上具有相应形状的(可选)属性。
现在让我们编写一个通用的assign()函数,它执行你在for循环中所做的赋值操作:
function assign<K extends keyof Base>(
k: K,
expl: Explicit<K>,
objk: Base[K] | { value: Base[K] }
) {
if (typeof objk === 'object') {
expl[k] = objk // okay
} else {
expl[k] = { value: objk } // okay
}
}
这可以在没有错误的情况下编译,因为在这两种情况下,你都将类型为 {value: Base[K]} 的值分配给相同泛型类型的属性。你可以看到,你获得的类型安全比类型断言更多,因为编译器会对将随机其他内容分配给 expl[k] 提出警告:
const obj: Explicit = null!
expl[k] = obj; // error!
一旦你有了 assign(),你就可以在 convert() 中调用它:
function convert(obj: Implicit) {
const expl: Explicit = {};
let k: keyof typeof obj;
for (k in obj) {
assign(k, expl, obj[k]); // okay
}
}
请注意,你并不需要声明一个单独的 assign() 函数,但你确实需要在某处有一个通用函数。它可以是 forEach() 的回调,或者一个内联函数:
function convert(obj: Implicit) {
const expl: Explicit = {};
let k: keyof typeof obj;
for (k in obj) {
(<K extends keyof Base>(
k: K, expl: Explicit<K>, objk: Base[K] | { value: Base[K] }
) => expl[k] = (typeof objk === 'object') ? objk : { value: objk })(
k, expl, obj[k]
);
}
}
就是这样!这个重构可能不值得麻烦;如果你足够小心,类型断言也是可以的。
代码的 Playground 链接
