typescript类型定义问题?

代码如下,这是一段示例代码

type MenuItem<T extends string> =
    {
        name: string;
        command: T;
    }
    |
    {
        name: string;
        command: string;
        children: Array<{
            name: string;
            command: T;
        }>;
    }

type CommandType = 'a' | 'b' | 'c'

const menu: Array<MenuItem<CommandType>> = [];
const result: Array<MenuItem<CommandType>> = [];

for (let s = 0; s < menu.length; s++) {
    const level1 = menu[s];

    if (level1.children && Array.isArray(level1.children) && level1.children.length > 0) {
        const childrenList = level1.children;
        const children: MenuItem<string>['children'] = [];

        for (let a = 0; a < childrenList.length; a++) {
            if (childrenList[a].command) {
                children.push({
                    name: childrenList[a].name,
                    command: childrenList[a].command
                });
            }
        }
        if (children.length > 0) {
            result.push({
                name: level1.name,
                command: level1.command,
                children
            });
        }
    } else if (level1.command) {
        result.push({
            name: level1.name,
            command: level1.command
        });
    }
}

那段for循环是想过滤有效数据。

问题是这段代码ts报错,代码逻辑没问题,问题的根本出在MenuItem的类型定义上,本意是想定义一个二级级联数据MenuItem,之所以使用T extends string而不是直接写为string,是因为在某些交互函数的参数中,希望将代表command的参数定义一个固定的可选数据CommandType。

当有子级数据的时候,关心的是子级数据的command,父级数据的command不希望受到CommandType的制约,随便写字符串都可以;没有子级数据时,关心的是第一级数据的command,这时command希望受到CommandType的制约

现在MenuItem的这种定义方式导致下面的代码ts报错,应该怎么定义MenuItem才能在达到目的的同时ts还不报错呢?

阅读 935
avatarAI BotBETA

为了解决类型定义的问题,你可以对 MenuItem 的类型定义做一些调整,以区分具有 children 属性的项(其子级 command 类型为 string)和不具有 children 属性的项(其 command 类型为 CommandType)。

一种方法是使用条件类型(conditional types)和类型别名(type aliases)来创建一个能够处理不同情况的类型。但是,TypeScript 并没有直接的语法来在单个类型定义中基于是否存在某个属性来应用不同的类型约束。因此,我们需要创建两个独立的类型,并在需要时根据条件将它们组合起来。

不过,在你的具体场景中,由于我们关心的是 children 属性的存在性,我们可以将 MenuItem 定义为一个泛型类型,该类型接受一个类型参数 C(代表子级 command 的类型),并在需要时将其设置为 stringCommandType

下面是一个修改后的 MenuItem 类型定义,它应该可以满足你的需求:

type MenuItemNoChildren<C extends string> = {
    name: string;
    command: C;
};

type MenuItemWithChildren<C extends string> = {
    name: string;
    command: string; // 不受CommandType的制约
    children: Array<MenuItemNoChildren<C>>; // 子级command使用相同的泛型参数
};

// 合并两个类型定义,使用条件类型判断是否有children属性
type MenuItem<C extends string> =
    | (MenuItemNoChildren<C> & { children?: undefined })
    | (MenuItemWithChildren<C> & { children: non-null });

// 注意:TypeScript中没有直接的"non-null"类型,但我们可以使用Exclude等技巧来模拟
// 这里我们简单地假设如果children存在,它就不是null或undefined

// 定义CommandType保持不变
type CommandType = 'a' | 'b' | 'c';

// 使用MenuItem类型时,为没有children的项指定CommandType,为有children的项指定string
const menu: Array<MenuItem<CommandType | string>> = [];
const result: Array<MenuItem<CommandType | string>> = [];

// ...(其余代码保持不变)

但是,请注意,由于 TypeScript 没有直接的方式来表达 "如果 children 存在则它是非空的,并且 command 应该是 string,如果 children 不存在则 command 应该是 CommandType" 这样的条件,上面的代码使用了 CommandType | string 作为折衷方案。这意味着对于没有 children 的项,command 可以是 CommandType 中的任何值,也可以是任何字符串,这可能不是你想要的最严格的类型检查。

在实际应用中,你可能需要根据你的具体需求来调整这个类型定义,或者考虑使用不同的数据结构或类型系统来更精确地表达你的意图。

1 个回答

in运算符来收窄

if ('children' in level1) {
    const childrenList = level1.children;
    const children: typeof level1['children'] = [];
    ...
} else {
    ...
}
推荐问题
logo
Microsoft
子站问答
访问
宣传栏