typescript :联合类型到可选值的深度交集(难度:95+)

标签 typescript types

我需要将联合对象类型(可能有嵌套联合)转换为 可选值的深度交集 类型。基本上所有可能的字段都将相交,并且仅当它存在于联合的一侧时才是可选的 - 并对所有嵌套对象执行此操作。

注意:这不是一个简单的并集到交集

内联补充意见:

type DateOnly = string;
type DayOfWeek = string;
type DayOfMonth = string;

// Input Type:
type DateIntervals_Union = {
    kind: 'weekly';
    weekly: {
        startDayOfWeek: DayOfWeek;
        endDayOfWeek: DayOfWeek;
        startDate: DateOnly;
        endDate?: DateOnly;
    };
} | {
    kind: 'monthly';
    monthly: {
        startDayOfMonth: DayOfMonth;
        endDayOfMonth: DayOfMonth;
        startDate: DateOnly;
        endDate?: DateOnly;
    };
} | {
    kind: 'dates';
    dates: {
        dateRanges: {
            startDate: DateOnly;
            endDate: DateOnly;
        }[];
    };
};

// Expected Type:
type DateIntervals_Optionals = {
    // This becomes a union of it's possible values
    kind: 'weekly' | 'monthly' | 'dates';
    // This becomes a union between the object and undefined
    weekly?: {
        // These are unchanged
        startDayOfWeek: DayOfWeek;
        endDayOfWeek: DayOfWeek;
        startDate: DateOnly;
        endDate?: DateOnly;
    };
    monthly?: {
        startDayOfMonth: DayOfMonth;
        endDayOfMonth: DayOfMonth;
        startDate: DateOnly;
        endDate?: DateOnly;
    };
    dates?: {
        dateRanges: {
            startDate: DateOnly;
            endDate: DateOnly;
        }[];
    };
};

// Input Type:
type Schedule_Union = {
    kind: 'once';
    date: DateOnly;
} | {
    kind: 'recurring';
    schedule: DateIntervals_Union;
};

// Expected Type:
type Schedule_Optionals = {
    // Union of possible values
    kind: 'once' | 'recurring';
    // Union of possible values: date | undefined
    date?: DateOnly;
    // Union of possible values: schedule | undefined
    // But the same union => optional type conversion is applied in this nested object
    schedule?: {
        kind: 'weekly' | 'monthly' | 'dates';
        weekly?: {
            startDayOfWeek: DayOfWeek;
            endDayOfWeek: DayOfWeek;
            startDate: DateOnly;
            endDate?: DateOnly;
        };
        monthly?: {
            startDayOfMonth: DayOfMonth;
            endDayOfMonth: DayOfMonth;
            startDate: DateOnly;
            endDate?: DateOnly;
        };
        dates?: {
            dateRanges: {
                startDate: DateOnly;
                endDate: DateOnly;
            }[];
        };
    }
};

// Simple UnionToIntersection does not work:
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Schedule_UnionToIntersection = UnionToIntersection<Schedule_Union>;
type Schedule_UnionToIntersection_Actual = {
    kind: 'once';
    date: DateOnly;
} & {
    kind: 'recurring';
    schedule: DateIntervals_Union;
};

// Partial<UnionToIntersection> does not work:
type PartialUnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? Partial<I> : never;
type Schedule_PartialUnionToIntersection = PartialUnionToIntersection<Schedule_Union>;
type Schedule_PartialUnionToIntersection_Actual = {
    kind?: undefined;
    date?: string | undefined;
    schedule?: {
        kind: 'weekly';
        weekly: {
            startDayOfWeek: string;
            endDayOfWeek: string;
            startDate: string;
            endDate?: string | undefined;
        };
        // Not nested
    } | {
        kind: 'monthly';
        // Um.. no, that's not right - where did that even come from?
        weekly: {
            // Bonus points if you can figure out how to make vscode show the full type information
            //...;
        };
    } | {
        //...;
    } | undefined;
};



(更新时间:2020-02-17)

目的

这允许使用更直接的模式来提取数据:
// BAD: This is not nice when needing to extract a single value
const funWith_unions = (dateIntervals_union: DateIntervals_Union) => {
    // If won't be null eventually :P
    let startDate: string = null as unknown as string;
    if (dateIntervals_union.kind === 'dates') {
        startDate = dateIntervals_union.dates.dateRanges[0]?.startDate;
    } else if (dateIntervals_union.kind === 'monthly') {
        startDate = dateIntervals_union.monthly.startDate;
    } else {
        startDate = dateIntervals_union.weekly.startDate;
    }
    if (!startDate) { throw new Error('No start date'); }
}

// GOOD: Quick, simple, and safe
const funWith_wide = (dateIntervals_wide: Widen<DateIntervals_Union>) => {
    // All possible cases handled in a single statement
    const startDate = dateIntervals_wide.dates?.dateRanges[0].startDate
        ?? dateIntervals_wide.monthly?.startDate
        ?? dateIntervals_wide.weekly?.startDate
        ?? (() => { throw new Error('No start date'); })()
}

// The above uses the excellent code from @jcalz (the accepted answer):

type AllKeys<T> = T extends any ? keyof T : never;
type OptionalKeys<T> = T extends any ?
    { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T] : never;
type Idx<T, K extends PropertyKey, D = never> =
    T extends any ? K extends keyof T ? T[K] : D : never;
type PartialKeys<T, K extends keyof T> =
    Omit<T, K> & Partial<Pick<T, K>> extends infer O ? { [P in keyof O]: O[P] } : never;
type Widen<T> =
    [T] extends [Array<infer E>] ? { [K in keyof T]: Widen<T[K]> } :
    [T] extends [object] ? PartialKeys<
        { [K in AllKeys<T>]: Widen<Idx<T, K>> },
        Exclude<AllKeys<T>, keyof T> | OptionalKeys<T>
    > :
    T;

最佳答案

好的,我会调用你正在做的事情Widen因为想要一个不太长的更好的名字。如果我理解正确,考虑您正在做什么的一种方法是采用对象类型的联合并假设如果联合成员的声明类型中不存在属性,则它实际上不存在(特别是,类型为 neverundefined 的可选属性)。

所以像 {foo: string, baz: true} | {bar: number, baz: false} 这样的类型可以认为是 {foo: string, bar?: never, baz: true} | {foo?: never, bar: number, baz: false} .然后你想要做的是将它们合并成一个单一类型,按照通常的规则将每个属性合并,并且每个属性是可选的,当且仅当它在联合的至少一个成员中是可选的,例如: {foo?: string, bar?: number, baz: boolean} .

并且您通过对象类型递归地执行此操作。

这是我可能会尝试写的一种方式。我会提到我在做什么但不一定
关于它如何工作的细节,因为否则这可能是十页文本:

首先让我们定义一个类型 AllKeys<T>分发 keyof跨工会所以 AllKeys<{a: string} | {b: number}>"a" | "b" :

type AllKeys<T> = T extends any ? keyof T : never;

然后我们写一个类型 OptionalKeys<T>仅标识类型中的可选键(并且它还分布在联合中),因此 OptionalKeys<{a?: string, b: number} | {c: boolean, d?: null}>应该是 "a" | "d" :
type OptionalKeys<T> = T extends any ?
  { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T] : never;

那我们写一个类型Idx<T, K, D>查找 K类型的属性 T ,除了它跨联合分布,如果没有这样的属性,它返回默认类型 D .所以,Idx<{a: string} | {b: number}, "a", 100>应该是 string | 100 :
type Idx<T, K extends PropertyKey, D = never> =
  T extends any ? K extends keyof T ? T[K] : D : never;

还有一种叫做 PartialKeys<T, K> 的类型就像 Partial<T>除了它只作用于键 K并将其他键留在 T 中独自的。所以Partial<T, keyof T>Partial<T> 相同, 和 Partial<T, never>T 相同:
type PartialKeys<T, K extends keyof T> =
  Omit<T, K> & Partial<Pick<T, K>> extends infer O ? { [P in keyof O]: O[P] } : never;

最后是 Widen<T> :
type Widen<T> =
  [T] extends [Array<infer E>] ? { [K in keyof T]: Widen<T[K]> } :
  [T] extends [object] ? PartialKeys<
    { [K in AllKeys<T>]: Widen<Idx<T, K>> },
    Exclude<AllKeys<T>, keyof T> | OptionalKeys<T>
  > :
  T;

我们对数组进行特殊处理,因为对数组的映射是 treated specially by the compiler ,否则我们只映射对象类型而不是基元(例如,您不想看到映射 string 时会发生什么)。但总体计划是:获取 T 中任何地方提到的所有属性的联合。 , 如果它们是可选的或在 T 的任何元素中缺失,则将它们设为可选.它可能无法在对象和非对象类型的联合类型上完美运行,但我认为它对您的示例做了正确的事情。

让我们来看看:
type WidenedScheduleUnion = Widen<Schedule_Union>
/* type WidenedScheduleUnion = {
    kind: "once" | "recurring";
    date?: string | undefined;
    schedule?: {
        kind: "weekly" | "monthly" | "dates";
        weekly?: {
            startDayOfWeek: string;
            endDayOfWeek: string;
            startDate: string;
            endDate?: string | undefined;
        } | undefined;
        monthly?: {
            ...;
        } | undefined;
        dates?: {
            ...;
        } | undefined;
    } | undefined;
} */

这看起来是对的,除了 ...因为类型太长了。让我们查看这些以查看更多详细信息:
type Monthly = NonNullable<WidenedScheduleUnion['schedule']>['monthly']
/* type Monthly = {
    startDate: string;
    startDayOfMonth: string;
    endDayOfMonth: string;
    endDate?: string | undefined;
} | undefined */

type Dates = NonNullable<WidenedScheduleUnion['schedule']>['dates']
/* type Dates = {
    dateRanges: {
        startDate: string;
        endDate: string;
    }[];
} | undefined */

这就是你想要的,对吧?好的,希望能帮助你继续。祝你好运!

Playground link to code

关于 typescript :联合类型到可选值的深度交集(难度:95+),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60114191/

相关文章:

mongodb - (节点 :18560) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'typeFn' of undefined

javascript - 如何从 npm 声明 javascript 子模块的类型?

javascript - typesafe 使用 reactjs 和 typescript 选择 onChange 事件

swift - 为什么 Swift 1.2 编译器不强制对 AnyObject 进行类型检查?

c - GCC 与 C11 标准中的位字段类型

angularjs - 到 child 的 Angular 路由 - 来自不同 parent 的 child

reactjs - 在 Jest 中编写手动模拟时,如何键入调用 genMockFromModule 的结果?

types - 类型推理规则

c++ - 如何在 QML 中创建自定义基本类型?

c++ - 存储不同类型的数据c++