首页 > 解决方案 > 在映射条件类型中排除内置函数

问题描述

考虑以下...

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

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

标签: typescript

解决方案


我不确定在类型系统中如何区分“内置”对象类型和“普通”对象类型,后者没有这样的概念。可能您可以构建一个大型硬编码的内置类型联合来检查并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所以如果TBoxed<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,因为我们没有拆箱函数返回值,对吗?)。该物业boxsubProp.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应该按原样返回而不是拆箱。你的代码中会有这么奇怪的类型吗?可能不是。但是,仍然需要注意。


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

链接到代码


推荐阅读