在映射条件类型中排除内置



请考虑以下几点...

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>匹配的未装箱内置。例如,日期、正则表达式、地图、集合等。映射不是映射内置,而是继续在原型上。这会导致类型不匹配,因为接口匹配,但不是实际的内置类型。

例如,以下代码行...

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 | ...这样的子句。 呸。

另一种可能的方法(在投入任何生产代码之前需要进行大量测试(是做出这样的假设:假设我在类型T上使用我的Unboxed<T>,并且我得到了一个新类型,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对某些RBoxed<R>,我们会得到R。 如果T是函数或基元类型,我们得到T. 否则,如果T是某种对象类型,我们会计算{[P in keyof T]: Unboxed<T[P]>}并将其分配给O以便于重用。 最后,我们将OT进行比较。 如果OT的某个更宽的版本,请使用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,因为我们没有取消装箱函数返回值,对吧? 属性boxsubProp.box也按预期未装箱。


这可能是我能得到的最接近你所要求的。 同样,您需要在使用之前对其进行彻底测试...有一些奇怪的边缘情况不能很好地适应这个定义。 假设T extends Unboxed<T>意味着内部没有任何东西可以拆箱,T可能是错误的,因为具有特制属性的对象看起来像某些UU & Boxed<U>。 像这个:

interface What {
foo: {
bar: string,
value: { bar: string }
}
}
type Wait = Unboxed<What>;
// type Wait = What

在这里,Whatfoo属性看起来像{bar: string} & Boxed<{bar: string}>,因此Unboxed<What>的定义错误地决定What应按原样返回,而不是将其拆箱。 你的代码中会有这么奇怪的类型吗? 应该不会。 但是,这仍然是需要注意的事情。


好的,希望这有所帮助,或者至少让您知道如何进行。 祝你好运!

链接到代码

最新更新