使用该只读库在代码中扩展库包中定义的普通对象的可区分联合

Extending discriminated union of plain objects defined in library package in code using that read-only lib

我有一个将纯 JS 对象写入存储的库。这些对象都有类型。我使用具有共同 "type" 属性.

的可区分联合

问题是使用该库(然后在 node_modules/ 下)的代码无法更新联合。

我刚刚将项目从 Flow 切换到 TypeScript。在 Flow I "solved" 中,它通过在 flow-typed/ 中提供全局类型。我在库代码的 flow-typed/ 目录中有一个空的全局类型,但它被应用程序自己的 flow-typed/ 全局定义覆盖(实际上忽略了整个文件)。

但是,在 TypeScript 中,我不认为我可以同时拥有一个(空的)局部类型,一个包含在联合类型定义中,然后被应用程序覆盖?

我通读了很多类似的问题,但它们总是有点偏差,建议的解决方案行不通,或者我只是不明白如何针对我的情况重写它们。一个区别是我自己的用例似乎更简单:没有 类,一点也不花哨,只是一堆 plain JavaScript objects.

Here is a playground link with an example approach (which fails).

这里的代码与示例略有不同,未显示尝试的方法,但仅显示其中的内容:

// ============================================================
// LIBRARY
// ============================================================

// Core (plain) object types defined within the library itself 
interface O1 {
    type: 'O1';
    a: string;
}
interface O2 {
    type: 'O2';
    b: number;
}

type oUnion = O1 | O2;

// Let's have a function to test the union on
declare function libraryFn<T extends oUnion>(p: T): T;

// Works (good)
const { b } = libraryFn({ type: 'O', b: 42 });
// Doesn't work (good)
libraryFn({ type: 'noSuchType' });

// ============================================================
// CODE THAT USES LIBRARY
// ============================================================

// HOW DO I ADD THIS TO THE UNION?
interface MyType {
    type: 'myO1';
    someProp: string;
}

const o: MyType = { type: 'myO1', someProp: 'a word' };
const {someProp} = libraryFn(o);

不幸的是,该示例中使用的使用通用成员的方法不起作用。

我当然可以通过包含一个通用的工会成员使其更容易工作,但这会使整个受歧视的工会变得无用。现在,当接受联合的函数获得一个特定的联合成员时,其确切的成员类型反映在 return 类型中。因此,如果一个成员的类型为 "Person",带有 email 属性,则类型检查器知道 returned 对象将具有 email 属性 因为它可以决定这里使用的是哪个联合成员。

我尝试了很多方法,但我设法实现的只是不断增加的复杂性,但没有解决方案。我在 Flow 中使用的 hacky 方法,覆盖应用程序中全局定义的类型变量,似乎是最不古怪的方法但是 a) 我不知道如何在 TS 中做同样的事情,b) 我不知道是否有有更好的吗?

如果您愿意修改库的类型定义(在上游进行此类更改,或者更有可能使用库定义的本地更改版本),您可以使 oUnion 类型可配置由库的用户(假设库在运行时实际上可以处理任意值,而不仅仅是库知道的类型)。

通常您可以使用 declaration merging 来自定义库提供的类型。但是声明合并只能 扩展 接口,这意味着它可以添加新的 properties/methods 并通过 使这些类型更 specific缩小 它们。您要做的是采用 oUnion 类型并通过 widening 使其更 general。声明合并不直接支持这一点。此外,oUnion 是一个类型别名,它也不能通过声明合并来改变,即使你想缩小它。

幸运的是,您可以重构代码以公开从中派生 oUnion 的接口,并且您可以将新属性合并到该接口中,这将导致 oUnion 以您的方式扩展预计。这是一个例子。

库代码如下所示:

module Library {
    // Core (plain) object types defined within the library itself 
    export interface O1 {
        type: 'O1';
        a: string;
    }
    export interface O2 {
        type: 'O2';
        b: number;
    }
    export interface O3 {
        type: 'O3';
        a: number;
    }
    // here's the interface you should merge stuff into
    export interface SupportedInterfaces {
        O1: O1,
        O2: O2,
        O3: O3
    }

    export type oUnion = SupportedInterfaces[keyof SupportedInterfaces];

    // Let's have a function to test the union on
    export declare function libraryFn<T extends oUnion>(p: T): T;

}

然后库的用户将合并到 SupportedInterfaces 声明中:

// Works (good)
const { a } = Library.libraryFn({ type: 'O3', a: 42 });
// Doesn't work (good)
Library.libraryFn({ type: 'noSuchType' });

// use declaration merging:
module Library {
    export interface SupportedInterfaces {
        OMyType: { type: 'myType', cc: 'cc' };
    }
}

Library.libraryFn({ type: 'myType', cc: "cc" }); // okay
Library.libraryFn({ type: 'myType', cc: "nope" }); // error, incompatible

Playground link

希望这能为您提供一条可行的前进道路。祝你好运!