在使用泛型的 TypeScript 类中处理动态键名

huangapple go评论52阅读模式
英文:

Working with Dynamic Keys in a Typescript Class using Generics

问题

I understand your code-related issue. You're facing a problem where TypeScript is not recognizing TreeContent[ChildKey] as an array type, even though you've checked it explicitly. This is because TypeScript can't infer the relationship between TreeContent and the specific keys like ChildKey. To achieve the behavior you want, you can use type assertions as a workaround.

In your case, you can inform TypeScript about the expected types by asserting the type of children and data. Here's how you can modify your code:

const children = treeContent[this.childrenKey] as TreeContent[ChildKey][];
const data = treeContent[this.dataKey] as Data;

By explicitly casting children and data, TypeScript should recognize them as the desired types.

Remember to make similar changes where necessary for other variables and properties in your code.

Please let me know if you need further assistance with your TypeScript code.

英文:

I'm running into an issue with the type system in TS when using extends keyof.
In this case I have a TreeContent object, however I want the tree to allow dynamic keys of where the data it is held in the tree object that the constructor uses.

I'm calling the class like so

const tree = new Tree<
            TestTree,
            "children",
            "data",
            TestTreeData,
            "label"
        >(treeTestData, "children", "data", "label");

The children key is to access the children data inside of the TestTree and the same with data and label, however, label will be used to find the label inside the TestTreeData. My aim here is to create a flexible class that can construct an n-ary tree from tree data that could use different aliases for children and the data it stores.

export class Tree<
    TreeContent,
    ChildKey extends keyof TreeContent,
    DataKey extends keyof TreeContent,
    Data,
    LabelKey extends keyof Data
>

{
    id: string;
    children: NodeModel<Data>[];
    childrenKey: ChildKey;
    dataKey: DataKey;
    labelKey: LabelKey;
    data: Data;

constructor(
        treeContent: TreeContent,
        _childrenKey: ChildKey,
        _dataKey: DataKey,
        _labelKey: LabelKey,
        _id = uuidv4()
    ) {
        this.dataKey = _dataKey;
        this.childrenKey = _childrenKey;
        this.labelKey = _labelKey;
        this.children = [];
        this.id = _id;

       if (
           !(_childrenKey in treeContent) ||
           !Array.isArray(treeContent[this.childrenKey])
          ) {
              throw Error(`Could not find data in ${String(this.childrenKey)}`);
          }

          const children = treeContent[this.childrenKey];

}

I omitted some of the other code in the constructor because I don't feel like it's relevant to the problem.

The issue is that children is const children: TreeContent[ChildKey] is not a type of an array even though I explicitly check if it's an array. I'm aware I can cast it however, I'm trying my best to avoid casting in my case. If that's the option I'm left with then that's fine but I can't figure out what I'm doing wrong.

I may be coming at this the completely wrong way, which I suspect.

What I want is that I can inform TS that I expect the value of const children: TreeContent[ChildKey] to become const children: TreeContent[]
Then I could replicate the logic with the DataKey but TreeContent[DataKey] should equal a Generic data object.

EDIT:
Here is a TS Playground with an error showing that I can't use the push function.

答案1

得分: 1

以下是您要的翻译部分:

"ChildKey" 和 "DataKey" 这两个泛型类型参数已被约束为 "TreeContent" 类型参数的键。

对于 "DataKey" 键的值类型,您没有进一步的约束,因此似乎没有理由使用 "Data" 类型参数;相反,您可以直接使用 "TreeContent[DataKey]" 来引用您之前称为 "Data" 的类型。

另一方面,您需要约束 "ChildKey" 键的值类型,以确保它是 "TreeContent" 数组。您可以通过将 "TreeContent" 约束为 "Record<ChildKey, TreeContent[]>" 来实现这一点,使用 "Record<K, V>" 实用类型。这并不意味着 "TreeContent" 不能拥有除了 "ChildKey" 之外的更多属性;在 TypeScript 中,对象类型是开放的,允许拥有更多未提及的属性。这只是意味着 "ChildKey" 必须是其中之一的键,其属性必须可分配给 "TreeContent[]"。

一旦您进行了这些更改,事情应该开始按预期运行:

class Tree<
    TreeContent extends Record<ChildKey, TreeContent[]>,
    ChildKey extends keyof TreeContent,
    DataKey extends keyof TreeContent,
    LabelKey extends keyof TreeContent[DataKey]
>{
    children: TreeContent[];
    childrenKey: ChildKey;
    dataKey: DataKey;
    labelKey: LabelKey;
    data: TreeContent[DataKey];

    constructor(treeContent: TreeContent, _childrenKey: ChildKey,
        _dataKey: DataKey, _labelKey: LabelKey ) {
        this.dataKey = _dataKey;
        this.childrenKey = _childrenKey;
        this.labelKey = _labelKey;
        this.children = [];

        const children = treeContent[this.childrenKey];
        children.push(); // okay

        this.data = treeContent[_dataKey]; // okay
    }
}

function createTree() {
    const tree = new Tree(testTreeData, "children", "data", "label");
    // const tree: Tree<TestTree, "children", "data", "label">
}

看起来不错。请注意,在调用 "Tree" 构造函数时,您不需要手动指定类型参数。编译器会自动正确推断它们。

代码的 Playground 链接

英文:

You already have that the ChildKey and DataKey generic type parameters are constrained to be keys of the TreeContent type parameter.

You don't have any further constraints on the type of value at the DataKey key, so there's no apparent reason for you to have a Data type parameter all all; instead you can just use the indexed access type TreeContent[DataKey] to refer to the type you were calling Data.

On the other hand, you need to constrain the type of value at the ChildKey key so that it is an array of TreeContent. You can do this by constraining TreeContent to Record&lt;ChildKey, TreeContent[]&gt;, using the Record&lt;K, V&gt; utility type. (This doesn't mean that TreeContent can't have more properties than just ChildKey; object types are open in TS and are allowed to have more properties than mentioned. It just means that ChildKey has to be one of the keys, and its property must be assignable to TreeContent[].

Once you make those changes, things should start behaving as desired:

class Tree&lt;
    TreeContent extends Record&lt;ChildKey, TreeContent[]&gt;,
    ChildKey extends keyof TreeContent,
    DataKey extends keyof TreeContent,
    LabelKey extends keyof TreeContent[DataKey]
&gt;{
    children: TreeContent[];
    childrenKey: ChildKey;
    dataKey: DataKey;
    labelKey: LabelKey;
    data: TreeContent[DataKey];

    constructor(treeContent: TreeContent, _childrenKey: ChildKey,
        _dataKey: DataKey, _labelKey: LabelKey ) {
        this.dataKey = _dataKey;
        this.childrenKey = _childrenKey;
        this.labelKey = _labelKey;
        this.children = [];
    
        const children = treeContent[this.childrenKey];
        // const children: TreeContent[ChildKey]    
        children.push(); // okay
    
        this.data = treeContent[_dataKey]; // okay
    }
}

function createTree() {
    const tree = new Tree(testTreeData, &quot;children&quot;, &quot;data&quot;, &quot;label&quot;);
    // const tree: Tree&lt;TestTree, &quot;children&quot;, &quot;data&quot;, &quot;label&quot;&gt;
}

Looks good. Note that you don't need to manually specify the type arguments when calling the Tree constructor. The compiler infers them correctly for you automatically.

Playground link to code

huangapple
  • 本文由 发表于 2023年5月24日 17:26:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76322003.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定