处于映射条件类型时排除内置

时间:2019-12-16 21:51:10

标签: 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>匹配的内置 unboxed 。例如,日期,正则表达式,映射,集合等。映射不会在内置对象上进行映射,而是继续在原型上进行。当接口匹配时,这会导致类型不匹配,但不会导致实际的内置类型。

例如,以下代码行...

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'."

如何使用仅对纯对象执行递归的递归条件映射类型?

1 个答案:

答案 0 :(得分:0)

我不确定如何期望类型系统中“内置”对象类型和“普通”对象类型之间的区别,而类型系统没有这种概念。可能您可以构建一个大型的内置类型的硬编码联合,以检查并使Unboxed<T>具有类似T extends Map<any, any> | Set<any> | Date | RegExp | ...的子句。好吧。

另一种可行的方式(在输入任何生产代码之前,您需要进行大量测试)是这样的假设:假设我将Unboxed<T>用于类型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的{​​{1}}是Boxed<R>,我们得到R。如果R是函数或原始类型,则得到T。否则,如果T是某种对象类型,我们将计算T并将其分配给{[P in keyof T]: Unboxed<T[P]>}以方便重用。最后,我们将OO进行比较。如果TO的较宽版本,请使用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而不是{ {1}},因为我们没有将函数返回值拆箱,对吗?属性{value: string}string也被取消了包装。


这可能是我最接近您要求的内容。同样,您需要在使用它之前对其进行彻底的测试。。。有些奇怪的情况在此定义中不能很好地发挥作用。对于具有某些box的{​​{1}}特制属性的对象,假设subProp.box暗示T extends Unboxed<T>内没有任何要取消装箱的操作是错误的。像这样一个:

T

在这里,U & Boxed<U>的{​​{1}}属性看起来像U,所以interface What { foo: { bar: string, value: { bar: string } } } type Wait = Unboxed<What>; // type Wait = What 的定义错误地决定应按原样返回foo拆箱。您的代码中会有这种奇怪的类型吗?可能不会。但是,仍然需要提防。


好的,希望对您有所帮助或至少能给您一些思路。祝你好运!

Link to code