For-in 循环将 TypeScript 类型转换为交集?
For-in loop converting TypeScript types to intersection?
编辑:更新的最小示例(希望如此):
归根结底,我想我好奇的核心是为什么这行不通,并导致以下两个错误:
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
,所以是在创建交集
将结果强制为两者?
上一题:
我正在尝试创建一个多功能函数,它接受 shorthand、隐式表示法 { someParameter: 123 }
或显式版本:{ someParameter: { value: 123, min: 0, max: 200 }}
,并转换和 returns显式版本:
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,
}
还有其他 ImplicitValue 类型(为简洁起见未列出),所以我想创建一个通用类型,即
自动转换为显式类型:
type MakeExplicit<T> = {
[K in keyof T]?: Extract<T[K], { value: any }>
}
这似乎可行,因为当我们使用@jcalz awesome 实用程序将鼠标悬停在类型上时,它是正确的:
但是,当我们尝试遍历 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]
然后结束它,还是有更好的方法来做到这一点?
Link to TS playground,如果有帮助的话。
这里的主要问题是,由于 k
属于 union type,因此编译器也将 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]
,那么分配的唯一安全的东西就是适用于 both 的东西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.
的主题
处理此问题的最简单方法就是使用 type assertion。当您对某些表达式的类型了解的比编译器多时,您可以直接告诉编译器这些信息。或者你可以告诉编译器不要担心 any
断言:
expl[k] = obj[k] as any; // I know what I'm doing
这很好,但是它将类型安全的一些责任从编译器上移开并交给了您。如果你写错了,编译器不会捕捉到它:
expl[k] = obj["someNumber"] as any; // oops
如果你想从编译器那里获得更多的类型安全保证,你将需要使用 generics 而不是联合,特别是你需要重构你的 Explicit
和 Implicit
类型是通用的并依赖于一个公共接口,因此赋值 expl[k] = objk
被视为一个赋值,其中双方在该接口和通用键 K
方面是相同的。
该方法在 microsoft/TypeScript#47109 中进行了描述,这是对 microsoft/TypeScript#30581 的修复(有趣的是,此处的重构无需更改该拉取请求即可工作,因为大多数机制已经成为语言)。
如果您有一个 t
类型 T
的对象,一个 generic 键类型 K
的键 k
,和类型 T[K]
的值 v
(也将是通用的),那么编译器 将 允许您编写 t[k] = v
,即使如果 K
是 non-generic 并集,则相同的赋值会失败。 (出于同样的原因,这证明是不安全的,但编译器允许它。只要我们注意不要用 union-type 指定 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>
并且编译器会知道它在相应形状的那个键上有一个(n 可选)属性。
现在让我们编写一个通用的 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]
);
}
}
好了!这种重构可能不值得麻烦;如果足够小心,类型断言也可以。
编辑:更新的最小示例(希望如此):
归根结底,我想我好奇的核心是为什么这行不通,并导致以下两个错误:
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
,所以是在创建交集
将结果强制为两者?
上一题:
我正在尝试创建一个多功能函数,它接受 shorthand、隐式表示法 { someParameter: 123 }
或显式版本:{ someParameter: { value: 123, min: 0, max: 200 }}
,并转换和 returns显式版本:
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,
}
还有其他 ImplicitValue 类型(为简洁起见未列出),所以我想创建一个通用类型,即 自动转换为显式类型:
type MakeExplicit<T> = {
[K in keyof T]?: Extract<T[K], { value: any }>
}
这似乎可行,因为当我们使用@jcalz awesome
但是,当我们尝试遍历 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]
然后结束它,还是有更好的方法来做到这一点?
Link to TS playground,如果有帮助的话。
这里的主要问题是,由于 k
属于 union type,因此编译器也将 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]
,那么分配的唯一安全的东西就是适用于 both 的东西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.
处理此问题的最简单方法就是使用 type assertion。当您对某些表达式的类型了解的比编译器多时,您可以直接告诉编译器这些信息。或者你可以告诉编译器不要担心 any
断言:
expl[k] = obj[k] as any; // I know what I'm doing
这很好,但是它将类型安全的一些责任从编译器上移开并交给了您。如果你写错了,编译器不会捕捉到它:
expl[k] = obj["someNumber"] as any; // oops
如果你想从编译器那里获得更多的类型安全保证,你将需要使用 generics 而不是联合,特别是你需要重构你的 Explicit
和 Implicit
类型是通用的并依赖于一个公共接口,因此赋值 expl[k] = objk
被视为一个赋值,其中双方在该接口和通用键 K
方面是相同的。
该方法在 microsoft/TypeScript#47109 中进行了描述,这是对 microsoft/TypeScript#30581 的修复(有趣的是,此处的重构无需更改该拉取请求即可工作,因为大多数机制已经成为语言)。
如果您有一个 t
类型 T
的对象,一个 generic 键类型 K
的键 k
,和类型 T[K]
的值 v
(也将是通用的),那么编译器 将 允许您编写 t[k] = v
,即使如果 K
是 non-generic 并集,则相同的赋值会失败。 (出于同样的原因,这证明是不安全的,但编译器允许它。只要我们注意不要用 union-type 指定 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>
并且编译器会知道它在相应形状的那个键上有一个(n 可选)属性。
现在让我们编写一个通用的 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]
);
}
}
好了!这种重构可能不值得麻烦;如果足够小心,类型断言也可以。