TypeScript 无法从已定义的字符串变量正确推断类型。

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

TypeScript is unable to infer type properly from a typed string variable

问题

我正在尝试使用一个“自由”创建的string作为对象的键。

interface CatState {
    name: string,
    selected: boolean,
    color: string,
    height: number
}

interface DogState {
    name: string,
    selected: boolean,
    race: string,
    age: number,
    eyeColor: string
}

export interface Animals {
    animals: {
        cat: {
            cats: CatState[],
            allSelected: boolean,
        },
        dog: {
            dogs: DogState[],
            allSelected: boolean,
        },
    }
};

const selectAnimal = (allAnimals: Animals, animal: keyof Animals['animals'], index: number) => {
    const animalPlural = `${animal}s` as keyof Animals['animals'][typeof animal]
    allAnimals.animals[animal][animalPlural][index].selected = true
}

这个部分突出显示了.selected,并显示以下消息:

属性 'selected' 在类型 'boolean' 上不存在。

是否有解决方法,或者这是否根本不可能?

英文:

I am trying to use a "freely" created string as a key for an object.

interface CatState {
    name: string,
    selected: boolean,
    color: string,
    height: number
}

interface DogState{
    name: string,
    selected: boolean,
    race: string,
    age: number,
    eyeColor: string
}

export interface Animals {
    animals: {
        cat: {
            cats: CatState[],
            allSelected: boolean,
        },
        dog: {
            dogs: DogState[],
            allSelected: boolean,
        },
    }
};

const selectAnimal = (allAnimals: Animals, animal: keyof Animals['animals'], index:number) => {
    const animalPlural = `${animal}s` as keyof Animals['animals'][typeof animal]
    allAnimals.animals[animal][animalPlural][index].selected= true
}

This highlights .selected with the message

> Property 'selected' does not exist on type 'boolean'.

Here is a Playground. Is there a workaround for this, or is this simply not possible?

答案1

得分: 1

你应该在 FooState.animals[type] 内部使用类似的变量名(例如 states),而不是 catsdogs。如果不这样做,TypeScript 将 catsdogs 视为两个不相关的对象。

这里 TypeScript playground 中可以看到示例。请注意,我已经删除了 PayloadAction 类型,因为 OP 没有提供它,并添加了对象解构的类型。

FooState 也可以从常规类型转换为接口。还请注意,我添加了一个 FooStateAnimalEntry 接口。添加此接口可以使代码更有组织性并提高可读性。

您可能需要更改 FooStateAnimalEntry.more 的类型,可能需要使用泛型来完成。

英文:

You should use a similar variable name (e.g. states) inside FooState.animals[type] instead of cats and dogs. Without doing so, TypeScript sees cats and dogs as two unrelated objects.

See this TypeScript playground. Note that I've the removed PayloadAction type since the OP did not provide it and added a type to the object destructuring.

FooState can also be converted from a regular type to an interface. Also notice I've add a FooStateAnimalEntry interface. Adding this interface gives you more organized code and improves readability.

You will probably want to change the type of FooStateAnimalEntry.more, which may need to be done with generics.

答案2

得分: 1

为了使这个工作,你需要将 selectAnimal 泛型 化。

你可能认为它应该能够处理 联合类型animal 输入,但编译器无法正确地对多个依赖相同联合类型的表达式进行类型检查。它失去了 `${animal}s`allAnimals.animals[animal] 之间的关联。前者的类型是 "cats" | "dogs",后者的类型是类似 {cats: CatState[]} | {dogs: DogState[]} 的类型,通常你不能用前者索引后者,因为"如果你有 {cats: CatState[]} 并且你正在用 "dogs" 进行索引呢?" 这是不可能的,但编译器无法看到这一点。TypeScript 无法直接处理这种相关联的联合类型。这是 microsoft/TypeScript#30581 的主题。

如果你想要一个单一的代码块适用于多个情况,那么类型需要重构为使用泛型,如 microsoft/TypeScript#47109 中所述。以下是你的示例可能会如何看起来:

interface AnimalStateMap {
    cat: CatState,
    dog: DogState
}

type AnimalData<K extends keyof AnimalStateMap> =
    { [P in `${K}s`]: AnimalStateMap[K][] & { allSelected: boolean } }

export interface Animals {
    animals: { [K in keyof AnimalStateMap]: AnimalData<K> };
};

const selectAnimal = <K extends keyof AnimalStateMap>(
    allAnimals: Animals, animal: K, index: number) => {
    const animalPlural = `${animal}s` as const;
    const animalData: AnimalData<K> = allAnimals.animals[animal]
    animalData[animalPlural][index].selected = true;
}

AnimalStateMap 是一个基本的键值类型,表示你的数据结构中的基本关系。然后 AnimalData<K> 是一个映射类型,使用 模板文字类型s 连接到键的类型上(给出了诸如 goosesfishs 这样的复数形式 🤷‍♂️),并且值类型是预期的动物数组。还有一个 allSelected 属性。

然后,你的 Animals 类型明确地写成了一个 映射类型,涵盖了 keyof AnimalStateMap,这将帮助编译器在我们进行 索引操作 时看到关联。

最后,selectAnimal 是泛型的,K extends keyof AnimalStateMap,并且其主体类型检查成功,因为 animalPlural 正好是 `${K}s` 的泛型类型,已知它是 animalData 的键,而 animalDataAnimalData<K>

在 Playground 上查看代码

英文:

In order for this to work you need to make selectAnimal generic.

You might think it should be able to deal with an animal input of a union type, but the compiler isn't able to properly type check a single block of code that uses multiple expressions that depend on the same union type. It loses track of the correlation between `${animal}s` and allAnimals.animals[animal]. The formers is of type &quot;cats&quot; | &quot;dogs&quot; and the latter is of a type like {cats: CatState[]} | {dogs: DogState[]}, and you can't generally index into the latter with the former, because "what if you've got {cats: CatState[]} and you're indexing with &quot;dogs&quot;?" That can't happen, but the compiler is unable to see it. TypeScript can't directly deal with correlated unions this way. That's the subject of microsoft/TypeScript#30581.

If you want a single code block to work for multiple cases, the types need to be refactored to use generics instead, as described in microsoft/TypeScript#47109. Here's how it might look for your example:

interface AnimalStateMap {
    cat: CatState,
    dog: DogState
}

type AnimalData&lt;K extends keyof AnimalStateMap&gt; =
    { [P in `${K}s`]: AnimalStateMap[K][] &amp; { allSelected: boolean } }

export interface Animals {
    animals: { [K in keyof AnimalStateMap]: AnimalData&lt;K&gt; };
};
    

const selectAnimal = &lt;K extends keyof AnimalStateMap&gt;(
    allAnimals: Animals, animal: K, index: number) =&gt; {
    const animalPlural = `${animal}s` as const;
    // const animalPlural: `${K}s`
    const animalData: AnimalData&lt;K&gt; = allAnimals.animals[animal]
    animalData[animalPlural][index].selected = true;
}

The AnimalStateMap is a basic key-value type representing the underlying relationship in your data structure. Then AnimalData&lt;K&gt; is a mapped type that encodes as a template literal type the concatenation of s onto the type of the keys (giving such plurals as gooses and fishs 🤷‍♂️) and that the value type is of the expected animal array. And that there's an allSelected property.

Then your Animals type explicitly written as a mapped type over keyof AnimalStateMap, which will help the compiler see the correlation when we index into it.

Finally, selectAnimal is generic in K extends keyof AnimalStateMap and the body type checks because animalPlural is of just the right generic type `${K}s` which is known to be a key of animalData, which is AnimalData&lt;K&gt;.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月1日 20:47:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76382024.html
匿名

发表评论

匿名网友

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

确定