typescript - 在映射条件类型中排除内置函数
问题描述
考虑以下...
type Boxed<T> = { value: T }
type Unboxed<T> = T extends Boxed<infer R>
? R
: T extends Record<any, any>
? { [P in keyof T]: Unboxed<T[P]> }
: T
function unbox(v: Record<any, any>): Unboxed {
}
unbox
是一个函数,它接受任何类型的对象并递归地拆箱所有装箱的对象。例如...
const output = unbox({ foo: { value: 'foo' }, bar: 'bar' })
output === { foo: 'foo', bar: 'bar' }
这很好用,除非其中一个属性是一个未装箱的内置函数,它也匹配extends Record<any, any>
. 例如,Dates、RegExp、Map、Set 等。不是映射内置函数,而是继续在原型上进行映射。这会导致类型不匹配,因为接口匹配,而不是实际的内置类型。
例如,以下代码行...
const unboxed: { date: Date } = Unboxed<{ date: Date }>
...引发此错误...
Type '{ date: { toString: {}; toDateString: {}; toTimeString: {}; toLocaleString: {}; toLocaleDateString: {}; toLocaleTimeString: {}; valueOf: {}; getTime: {}; getFullYear: {}; getUTCFullYear: {}; getMonth: {}; ... 32 more ...; getVarDate: {}; }; }' is not assignable to type '{ date: Date; }'.\n Types of property 'date' are incompatible.\n Property '[Symbol.toPrimitive]' is missing in type '{ toString: {}; toDateString: {}; toTimeString: {}; toLocaleString: {}; toLocaleDateString: {}; toLocaleTimeString: {}; valueOf: {}; getTime: {}; getFullYear: {}; getUTCFullYear: {}; getMonth: {}; ... 32 more ...; getVarDate: {}; }' but required in type 'Date'."
如何使用仅对普通对象执行递归的递归条件映射类型?
解决方案
我不确定在类型系统中如何区分“内置”对象类型和“普通”对象类型,后者没有这样的概念。可能您可以构建一个大型硬编码的内置类型联合来检查并Unboxed<T>
生成一个类似T extends Map<any, any> | Set<any> | Date | RegExp | ...
. 呸。
另一种可能的方法(在投入任何生产代码之前您需要进行大量测试)是做出这样的假设:假设我Unboxed<T>
在一个类型上使用 myT
并且我得到一个T
仍然可以分配的新类型。这可能意味着它Unboxed<T>
实际上并没有在T
. 如果是这样,那么我可能应该只使用T
而不是Unboxed<T>
给我的东西。所以如果T extends Unboxed<T>
,则返回T
。这种检查可能会被翻译成这样:
type Unboxed<T> =
T extends Boxed<infer R> ? R :
T extends Function ? T :
T extends object ? (
{ [P in keyof T]: Unboxed<T[P]> } extends infer O ? T extends O ? T : O : never) :
T
(我还决定不应该尝试拆箱Function
兼容的值)。T
所以如果T
是Boxed<R>
为了某些人R
,我们得到R
。如果T
是函数或原始类型,我们得到T
. 否则,如果T
是某个对象类型,我们计算{[P in keyof T]: Unboxed<T[P]>}
并分配给它以便O
于重用。最后,我们比较O
一下T
。如果O
是更广泛的版本T
,请使用T
. 否则,使用O
.
给定以下Example
界面:
interface Example {
str: string;
num: number;
box: Boxed<{ foo: string }>;
fun: () => { value: string };
dat: Date;
subProp: {
str: string;
dat: Date;
map: Map<string, number>;
set: Set<boolean>;
reg: RegExp;
box: Boxed<{ bar: number }>;
}
}
这是您拆箱时得到的结果:
type UnboxedExample = Unboxed<Example>;
/*
type UnboxedExample = {
str: string;
num: number;
box: {
foo: string;
};
fun: () => {
value: string;
};
dat: Date;
subProp: {
str: string;
dat: Date;
map: Map<string, number>;
set: Set<boolean>;
reg: RegExp;
box: {
bar: number;
};
};
}
*/
所有内置类型都没有改变,fun
函数值属性也是如此(请注意,这意味着返回类型fun
仍然是{value: string}
而不是string
,因为我们没有拆箱函数返回值,对吗?)。该物业box
也subProp.box
按预期拆箱。
这可能是我能得到的最接近你所要求的东西。同样,您需要在使用它之前对其进行彻底测试……有些奇怪的边缘情况与此定义不能很好地配合。T extends Unboxed<T>
暗示内部没有任何东西可以拆箱的假设T
可能是错误的,因为对象具有看起来像U & Boxed<U>
某些U
. 像这个:
interface What {
foo: {
bar: string,
value: { bar: string }
}
}
type Wait = Unboxed<What>;
// type Wait = What
在这里, 的foo
属性What
看起来像{bar: string} & Boxed<{bar: string}>
,因此Unboxed<What>
错误的定义决定What
应该按原样返回而不是拆箱。你的代码中会有这么奇怪的类型吗?可能不是。但是,仍然需要注意。
好的,希望对您有所帮助,或者至少让您知道如何继续。祝你好运!
推荐阅读
- linux - OpenSSL and CryptoJS SHA256 encryption conversion
- firebase - 限制用户访问移动网站
- node.js - 如何将 Int 值转换为从 mssql 到 EJS 的小时(时间)
- python - 指定 *.pyd 输出路径?
- wso2 - WSO2 ESB,如何使用聚合调解器或任何其他调解器组合 2 条消息
- r - 删除 R 中的特定行
- image - Google Drive encrypt image with Apps Script? (cCryptoGS)
- flutter - 如何在 Flutter 中对齐单个小部件?
- pine-script - 无法将音量条固定到图表底部
- runtime-error - 关于 Magento 2 管理员网址的问题