【问题标题】:How do I treat two children classes of the same parent class, as the same, yet different Type?如何将同一父类的两个子类视为相同但不同的类型?
【发布时间】:2021-05-08 22:01:49
【问题描述】:

我需要帮助来弄清楚如何在 typescript 中正确地断言类型。

我正在使用的代码让 rn 感到困惑(我认为我让它变得比它需要的更复杂,哈哈 :3),所以我将简要介绍一下我正在尝试完成的工作。

所以基本上我有一个实现 IAbstractFactory 接口的 ConcreteFactory 类。 ConcreteFactory 类可以创建/生成 3 个不同的具体类 BaseDocument、DocumentA 和 DocumentB。

interface IAbstractFactory{
    createBaseDocument(): Promise<IAbstractDocument>;
    createDocumentA(): Promise<IAbstractDocument>;
    createDocumentB(): Promise<IAbstractDocument>;
}

class ConcreteFactory implements IAbstractFactory{
    public async createBaseDocument(): Promise<IAbstractDocument> {
        return new BaseDocument();
    }

    public async createDocumentA(): Promise<IAbstractDocument> {
        return new DocumentA();
    }

    public async createDocumentB(): Promise<IAbstractDocument> {
        return new DocumentB();
    }
}

请注意,只有 Document 类实现了 IAbstractDocument 接口。 DocumentA 和 DocumentB 都扩展了 BaseDocument。

interface IAbstractDocument{
    methodBase(){};
}

class BaseDocument implements IAbstractDocument{
    methodBase(){};
}

// uses all of BaseDocuments functions but has some extra functions that DocumentB does not have
class DocumentA extends BaseDocument{
    methodBase(){}; 
    methodA(){}; // not implemented in IAbstractDocument, throws error
}

// uses all of BaseDocuments functions but has some extra functions that DocumentA does not have 
class DocumentB extends BaseDocument{
    methodBase(){}; 
    methodB(){}; // not implemented in IAbstractDocument, throws error
}

我还有一个名为 Tree 的类,它将构建一棵树,每个节点的类型都是 IAbstractDocument

class Tree{
    rootNode:IAbstractDocument; // Realistically of type DocumentA or DocumentB, IAbstractDocument is throwing error, explained below
    
    nodes: IAbstractDocument[]; // Realistically of type DocumentA or DocumentB, IAbstractDocument is throwing error, explained below

    // this function will create factory and the factory will create DocumentA or Document B
    createDocument(){}

    addDocument()

}

我的目标是创建一个文档树。规则是 DocumentA 可以包含子节点,类型为 DocumentA 或 DocumentB,但 DocumentB 不能有任何子节点(它们只能是叶节点)。

问题是:

虽然 DocumentA 和 DocumentB 都扩展了 BaseDocument。在 Tree 类中,它们在 IAbstractDocument 类型下定义(我这样做是因为我认为这就是 ConcreteFactory 返回的内容)。所以现在我得到一个错误,DocumentA 和 DocumentB(分别是methodA 和methodB)中的唯一函数没有在IAbstractDocument 中定义。所以我不能在 Tree 类中使用它们,我只能使用 methodBase()。

我的问题是:

如何将 DocumentA 和 DocumentB 视为相同但不同的类型?它们都应该被视为 中的文档节点树,但它们具有与它们相关的不同功能。

任何帮助将不胜感激,谢谢!

【问题讨论】:

  • 请考虑修改这里的代码以构成一个minimal reproducible example 适合放入像The TypeScript Playground 这样的独立IDE,以便其他人可以重现您的问题(您所说的任何错误都应该是明显的) 并且只有您的问题(应删除其他错误,如拼写错误或未声明的依赖项)。如果我可以立即开始解决问题而不必先解决问题,那么提供帮助会容易得多。
  • 我不知道在这里对Tree 说什么; nodes 不应该是其他 Tree 实例吗?如果您对子/叶区分有如此不同的要求,您可能希望Tree 本身是可子类化的,并具有DocumentATree 和DocumentBTree。
  • 我倾向于使用union types 并避免使用一般的基本/抽象版本。 this 是否为您工作或为您提供指导?如果是这样,我会写一个答案;如果不是,请详细说明您的问题。
  • 对不起,如果我没有给出一个可重现的例子,我应该考虑一下。 Union 类型确实有意义,我认为这就是我需要的答案。谢谢!但是,让节点成为其他 Tree 实例是什么意思?将树拆分为多个子树对性能更好吗?我试图重新创建此树遍历算法bfs-dfs-example 的功能,但我想用DocumentA 和/或DocumentB 替换TreeNode。
  • 关于这两种类型的文档,我还想澄清一件事。对不起,如果我没有很好地解释它,但 DocumentA 充当其他“子集合”(DocA)或“子项”(DocB)的一种“集合”(DocA)。而 DocumentB 只是作为一个项目,它可以单独存在(根节点)或者是“集合/子集合”(DocA)的子节点。所以树结构的一个例子可以是 DocA -> [DocB, DocB] OR DocA -> [DocA, DocA] -> [[DocB, DocB], [DocB, DocB]] 或 DocB

标签: typescript design-patterns types interface typescript-generics


【解决方案1】:

我认为您可能希望使用union types 来表示“非此即彼”,而不是超类或其他基类型,这些类型无法轻松检查。一般来说,您可能应该根据需要使用特定的类型,以使编译器知道您在做什么。

接下来我会将名称 DocumentA 更改为 DocumentCollection 并将 DocumentB 更改为 DocumentSingle,因为我认为这就是您的意图。 DocumentCollection 将具有 children 属性,而 DocumentSingle 将没有。这里是ConcreteFactory:

class ConcreteFactory {
    public async createBaseDocument(): Promise<BaseDocument> {
        return new BaseDocument();
    }

    public async createDocumentCollection(): Promise<DocumentCollection> {
        return new DocumentCollection();
    }

    public async createDocumentSingle(): Promise<DocumentSingle> {
        return new DocumentSingle();
    }
}

查看它如何在其返回签名中返回更具体的类型。现在让我们看看Document 类的可能实现:

class BaseDocument {
    methodBase() { };
    constructor(public docName: string = "") { }
}

class DocumentCollection extends BaseDocument {
    methodBase() { };
    methodCollection() { console.log("MethodCollection by " + this.docName) };
    children: Array<DocumentCollection> | Array<DocumentSingle> = [];
}

class DocumentSingle extends BaseDocument {
    methodBase() { };
    methodSingle() { console.log("MethodSingle by " + this.docName) };
}

我已经给文档一个docName 以便我们稍后可以看到一些有用的输出。注意DocumentCollection 的children 属性是Array&lt;DocumentCollection&gt; 还是Array&lt;DocumentSingle&gt;。这将防止它成为一个混合集合,如果我从你的问题中理解正确,你想禁止它。请注意,使用联合数组比使用联合数组更难,但它就是这样。

让我们为DocumentSingle 和DocumentCollection 的联合命名:

type Document = DocumentSingle | DocumentCollection;

如果您能提供帮助,您可能希望使用Document 而不是AbstractDocument 或BaseDocument。


现在让我们看看Tree:

class Tree {

    nodes: Document[]
    constructor(public rootNode: Document) {
        this.nodes = [];
        const addNodes = (x: Document): void => {
            this.nodes.push(x)
            if ("children" in x) {
                x.children.forEach(addNodes)
            }
        }
        addNodes(this.rootNode);

    }
    walk(
      processSingle: (x: DocumentSingle) => void, 
      processCollection: (x: DocumentCollection) => void
    ) {
        const walk = (x: Document): void => {
            if ("methodSingle" in x) {
                processSingle(x);
            } else {
                processCollection(x);
                x.children.forEach(walk)
            }
        }
        walk(this.rootNode);
    }
}

Tree 有一个 rootNode 类型为 Document。它还有一个nodes 类型为Document[] 的数组。当您使用其rootNode 构造Tree 时,构造函数还将遍历树以构建nodes 数组(如果重要)。还有一个walk() 方法,它为DocumentSingle 和DocumentCollection 获取处理器回调,并遍历调用适当处理器的树。

请注意,在addNodes() 和walk() 箭头函数的实现中,编译器使用DocumentSingle 或DocumentCollection 的特定属性之一的存在将Document 缩小为两名成员。这是因为the in operator can be used as a type guard。


所以让我们创建一个Tree:

const leafOne = new DocumentSingle("leafOne");
const leafTwo = new DocumentSingle("leafTwo");
const leafThree = new DocumentSingle("leafThree");
const leafFour = new DocumentSingle("leafFour");

const branchOne = new DocumentCollection("branchOne");
branchOne.children = [leafOne, leafTwo];

const branchTwo = new DocumentCollection("branchTwo");
branchTwo.children = [leafThree, leafFour];

const root = new DocumentCollection("root");
// note how I'm not allowed to assign a mixed thing to root.children
root.children = [branchOne, leafOne];

root.children = [branchOne, branchTwo]; // okay

const tree = new Tree(root);

从上面可以看出,DocumentCollection 的children 属性中不允许使用DocumentSingle 和DocumentCollection 的混合数组。您还应该注意,我重新分配了 children 属性与一个新的数组,而不是尝试将 push() 放到它上面。如果您尝试使用push(),您会发现它不会让您添加任何内容,因为它不知道数组是否包含DocumentSingles 或DocumentCollections。不确定这是否会破坏您的用例,但如果是这样,我们可以更改它。 (嗯,你可以改变它;我不知道我可以承诺它⏳)

太好了,我们有一个tree...让我们walk()吧:

tree.walk(x => x.methodSingle(), y => y.methodCollection());
/* [LOG]: "MethodCollection by root" 
[LOG]: "MethodCollection by branchOne" 
[LOG]: "MethodSingle by leafOne" 
[LOG]: "MethodSingle by leafTwo" 
[LOG]: "MethodCollection by branchTwo" 
[LOG]: "MethodSingle by leafThree" 
[LOG]: "MethodSingle by leafFour" */

太棒了!或者,我们可以迭代它的 nodes 属性:

tree.nodes.forEach(x => "methodSingle" in x ? x.methodSingle() : x.methodCollection());
/* [LOG]: "MethodCollection by root" 
[LOG]: "MethodCollection by branchOne" 
[LOG]: "MethodSingle by leafOne" 
[LOG]: "MethodSingle by leafTwo" 
[LOG]: "MethodCollection by branchTwo" 
[LOG]: "MethodSingle by leafThree" 
[LOG]: "MethodSingle by leafFour" */

看起来不错。


Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多