TypeScript - 递归泛型和叶分支问题 (Ultimate TicTacToe)

Posted

技术标签:

【中文标题】TypeScript - 递归泛型和叶分支问题 (Ultimate TicTacToe)【英文标题】:TypeScript - Recursive generics and leaf branch-like problem (Ultimate TicTacToe) 【发布时间】:2019-12-19 02:13:22 【问题描述】:

我正在制作一个Ultimate Tic-Tac-Toe 游戏并且我正在创建网格/板。

我如何递归地使用泛型让我的网格内部(在其单元格中)有其他网格? 深度级别未知,这可能有超过 2 的深度......它可能是一个网格的一个网格的一个网格的一个网格的网格,即

这与叶/分支问题大致相同。一个分支可以有叶子或其他分支(在这种情况下不能同时有)。

我已经尝试了一些方法(使用接口、定义自定义类型等),但它总是给出相同的错误(见下文,在错误部分)。

这是我最终得到的代码...

Cell.ts

import  CellValue  from "./CellValue";

export class Cell 
    private value: CellValue;
    constructor() 

    

    public getValue(): CellValue 
        return this.value;
    

井字游戏.ts

import  Cell  from "./Cell";

/**
 * Represents the Tic Tac Toe grid.
 */
export class TicTacToeGrid<T = TicTacToeGrid|Cell> 

    /**
     * The grid can contain cells or other grids
     * making it a grid of grids.
     */
    private grid: Array<Array<T>> = [];

    /**
     * Creates a new grid and initiailzes it.
     * @param [number, number][] sizes an array containing the sizes of each grid.
     * The first grid has the size defined in the first element of the array. i.e. [3, 3] for a 3x3 grid.
     * The others elements are the sizes for the sub-grids.
     */
    constructor(sizes: [number, number][]) 
        const size: [number, number] = sizes.splice(-1)[0]; // Get the size of this grid

        for(let y = 0; y < size[1]; y++) 
            this.grid.push();
            for(let x = 0; x < size[0]; x++) 
                this.grid[y].push();

                this.grid[y][x] = sizes.length > 0 ? new TicTacToeGrid(sizes) : new Cell(); // if there still are size left in the array, create a new sub-grid with these sizes
            
        

    

    /**
     * Returns the content of the specified cell
     * @param number x the x position of the cell whose content wants to be retrieved
     * @param number y the y position of the cell whose content wants to be retrieved
     */
    public getElement(x: number, y: number): T 
        return this.grid[y][x];
    

index.ts

import  TicTacToeGrid  from "./TicTacToeGrid ";
import  Cell  from "./Cell";

const board: TicTacToeGrid<TicTacToeGrid<Cell>> = new TicTacToeGrid<TicTacToeGrid<Cell>>([[3, 3], [5, 5]]);

const value: number= board.getElement(2, 2).getElement(1, 1).getValue();

一切正常,除了编译器在 TicTacToeGrid.js 中抛出 2 个错误:

    Type parameter 'T' has a circular default.

-> 好的,但是我该如何解决呢?

    Type 'Cell | TicTacToeGrid<unknown>' is not assignable to type 'T'. 'Cell | TicTacToeGrid<unknown>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint ''. Type 'Cell' is not assignable to type 'T'. 'Cell' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint ''.

-> 我该如何处理呢?究竟是什么意思?

【问题讨论】:

相关:***.com/a/45999529/135978 【参考方案1】:

问题 #1:循环默认

声明class TicTacToeGrid&lt;T = TicTacToeGrid|Cell&gt; 意味着T可以是任何东西,但是如果你只写类型TicTacToeGrid而不指定类型参数,它将使用TicTacToeGrid|Cell。这是一种不好的循环,因为编译器被要求急切地评估类型TicTacToeGrid&lt;TicTacToeGrid&lt;TicTacToeGrid&lt;...|Cell&gt;|Cell&gt;|Cell&gt;,它不知道该怎么做。您可以通过将其更改为 class TicTacToeGrid&lt;T = TicTacToeGrid&lt;any&gt;|Cell&gt; 来解决此问题,这会打破循环。

但是,我认为您实际上根本不想要default generic parameter。你不希望T 可能是string,对吧?您正在寻找的是 generic constraint,而是使用 extends 关键字:

class TicTacToeGrid<T extends TicTacToeGrid<any> | Cell>  ... 

现在你的行为会更合理。


问题 #2:'Cell | TicTacToeGrid&lt;...&gt;' is not assignable to type 'T'

这是由于一个长期存在的问题(请参阅microsoft/TypeScript#24085),其中扩展联合类型的泛型类型参数没有通过控制流分析缩小范围。如果你有一个具体的联合类型,比如string | number,你可以检查一个变量sn,比如(typeof sn === "string") ? sn.charAt(0) : sn.toFixed(),在三元运算符的分支内,sn 的类型将缩小为stringnumber。但是如果sn 是像T extends string | number 这样的泛型类型的值,那么检查(typeof sn === "string") 不会对T 类型做任何事情。在您的情况下,您想检查sizes.length 并将其缩小为TCell 或一些TicTacToeGrid。但这不会自动发生,编译器会警告您它无法确定您为网格元素分配了正确的值。它会看到您分配 Cell | TicTacToeGrid&lt;XXX&gt;,并警告您 T 可能比这更窄,而且不安全。

这里最简单的解决方法是通过type assertion 告诉编译器你确定你正在做的事情是可以接受的:

this.grid[y][x] = (sizes.length > 0 ? new TicTacToeGrid(sizes) : new Cell()) as T;

最后的as T 告诉编译器你有责任确保前面的表达式是一个有效的T。如果在运行时发现你错了,那么你已经用断言对编译器撒了谎,这就是你要处理的问题。事实上,这个问题很容易发生在您定义构造函数的方式中,因为无法从 sizes 参数推断出 T... 所以对编译器撒谎很简单:

const lyingBoard: TicTacToeGrid<
  TicTacToeGrid<TicTacToeGrid<Cell>>
> = new TicTacToeGrid([[3, 3]]); // no error

lyingBoard
  .getElement(0, 0)
  .getElement(0, 0)
  .getElement(0, 0)
  .getValue(); // no error at compile time, kablooey at runtime

但大概你不会那样做。可能有一种方法可以确保sizes 参数与T 匹配,或者甚至可以从sizes 参数中推断出T,但这使我们离你的问题更远,这个答案已经很长了够了。


Link to code

【讨论】:

以上是关于TypeScript - 递归泛型和叶分支问题 (Ultimate TicTacToe)的主要内容,如果未能解决你的问题,请参考以下文章

使用泛型和合并对象的 Typescript 声明文件

打字稿、泛型和重载

TypeScript 基础学习之泛型和 extends 关键字

Java 泛型和可变参数

泛型和枚举

泛型和枚举