英文:
Wildcard key on this scope inside a class with Typescript gives "property [x] does not exist on type..."
问题
I have a small npm package that groups all items inside one class so I can do something to every item in said class via the group (For example: I want to be able to individually call item.set(xx)
but I want to be able to do it via a group as well (group.item.set(xx)
) so I can basically make a function inside my group class that would set xx
to all items in said group.
我有一个小的npm包,将所有项目分组到一个类中,这样我就可以通过组对类中的每个项目进行操作(例如:我想能够单独调用 item.set(xx)
,但我也想能够通过组来调用它(group.item.set(xx)
),这样我就可以在我的组类中创建一个函数,将xx
设置给该组中的所有项目。
I have a dummy below which is used in the same manner (but very much stripped for this purpose). As you can see I give my Group
class an object with a key and a GroupItem
. In the constructor I then alias the key to be readily available via some kind of a shorthand (this[k] = items[k]
which basically means I can do group.first
instead of group.items.first
).
我下面有一个虚拟示例,用法类似(但为了这个目的而大大精简)。正如您所看到的,我给我的Group
类提供了一个带有键和GroupItem
的对象。然后在构造函数中,我将键别名为某种速记方式,以便通过速记方式轻松使用它(this[k] = items[k]
,这基本上意味着我可以执行group.first
而不是group.items.first
)。
This, however, throws an error because when I try to call group.first
after initializing the class it says Key first does not exist on type....
. Which is true, but I don't want my VScode to make a big hassle out of it because I want to be able to use any name (first
, second
or toilet
or whatever inside that group)
然而,这会引发错误,因为在初始化类后,当我尝试调用group.first
时,它会说Key first does not exist on type....
。这是真的,但我不希望我的VScode为此制造麻烦,因为我希望能够使用任何名称(first
、second
或toilet
或组内的任何其他名称)。
I've tried adding this to my GroupInterface
[key: string]: GroupItem; // or any/unknown/never, ive tried them all
without success (const group
only seems to see what's inside the class and skips the Interface on intellisense)
我尝试将这个添加到我的GroupInterface
中:
[key: string]: GroupItem; // or any/unknown/never, ive tried them all
但没有成功(const group
似乎只看到类内部的内容,并跳过Intellisense中的接口)。
Adding the above straight into my class does work somewhat; it won't show the error for second
but gives other issues such as (Property 'items' of type 'GroupItem[]' is not assignable to 'string' index type 'GroupItem'.ts(2411)
) and makes it a pain to actually use inside some code due to the extra typecasting you'll have to do to see if group.second
is not secretly an array etc.
将上述内容直接添加到我的类中确实有一些作用;它不会显示second
的错误,但会引发其他问题,如(Property 'items' of type 'GroupItem[]' is not assignable to 'string' index type 'GroupItem'.ts(2411)
),并且由于您必须进行额外的类型转换才能查看group.second
是否是一个数组等,因此在某些代码中实际使用起来非常麻烦。
These (obviously) throw the error Property 'first' does not exist on type 'Group'.ts(2339)
:
这些(显然)引发了错误 Property 'first' does not exist on type 'Group'.ts(2339)
:
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first);
console.log(theGroup.second);
In short:
Is there a way I can use whatever key I want and map directly to my this
scope without vscode telling me that that key does not exist?
简而言之:
是否有一种方法可以使用任何我想要的键,并直接映射到我的this
范围,而不让VScode告诉我该键不存在?
英文:
I have a small npm package that groups all items inside one class so I can do something to every item in said class via the group (For example: I want to be able to individually call item.set(xx)
but I want to be able to be able to do it via a group aswell (group.item.set(xx)
) so I can basically make a function inside my group class that would set xx
to all items in said group.
I have a dummy below which is used in the same manner (but very much stripped for this purpose). As you can see I give my Group
class an object with a key and a GroupItem
. In the constructor I then alias the key to be readily available via some kind of a shorthand (this[k] = items[k]
which basically means I can do group.first
instead of group.items.first
.
This, however, throws an error because when I try to call group.first
after initializing the class it says Key first does not exist on type....
. Which is true, but I don't want my VScode to make a big hassle out of it because I want to be able to use any name(first
, second
or toilet
or whatever inside that group)
I've tried adding this to my GroupInterface
[key: string]: GroupItem; // or any/unknown/never, ive tried them all
without success (const group
only seems to see what's inside the class and skips the Interface on intellisense)
Adding the above straight into my class does work somewhat; it won't show the error for second
but gives other issues such as (Property 'items' of type 'GroupItem[]' is not assignable to 'string' index type 'GroupItem'.ts(2411)
) and makes it a pain to actually use inside some code due to the extra typecasting you'll have to do to see if group.second
is not secretly an array etc.
class GroupItem {
name = 'A group item';
}
interface GroupItemInGroup {
[key: string]: GroupItem;
}
interface GroupInterface {
apply(): void;
}
class Group implements GroupInterface {
items: GroupItem[];
constructor(items: GroupItemInGroup) {
this.items = [];
// key the `items` object so we can create a shorthand (group.first)
Object.keys(items).forEach((k) => {
this.items.push(items[k]);
this[k] = items[k];
});
}
apply(): void {
throw new Error('Method not implemented.');
}
}
These(obviously) throw the error Property 'first' does not exist on type 'Group'.ts(2339)
:
const theGroup = new Group({
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first);
console.log(theGroup.second);
In short:
Is there a way I can use whatever key I want and map directly to my this
scope without vscode telling me that that key does not exist?
答案1
得分: 1
Conceptually you want Group
to be generic in the keys of the items
constructor argument, like class Group<K> implements Record<K, GroupItem> {⋯}
(using the Record<K, V>
utility type). But that's not allowed in TypeScript; interfaces and classes require statically known keys, and K
is not statically known. See microsoft/TypeScript#2225 and microsoft/TypeScript#21326.
如果你可以放弃使用类,而是编写一个工厂函数,那么这相对容易编写:
function Group<K extends PropertyKey>(items: Record<K, GroupItem>) {
const _items: { [k: string]: GroupItem } = items; // widen
return {
...items,
items: [...Object.values(_items)],
apply(): void {
throw new Error('Method not implemented.');
}
}
}
/* function Group<K extends PropertyKey>(items: Record<K, GroupItem>
): Record<K, GroupItem> & { items: GroupItem[]; apply(): void; }} */
返回的对象的类型由 TypeScript 自动计算为对象文字的各个部分的交集。现在当你调用它(而不是使用 new
)时,你会得到期望的行为:
const theGroup = Group({
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first); // okay
console.log(theGroup.second); // okay
console.log(theGroup.items); // okay
try { theGroup.apply() } catch (e) { console.log(e) } // okay
如果你需要使用类,那么你必须通过编写尽可能接近你想要的类的方式来模拟它,将其重命名(例如,_Group
),然后使用类型断言告诉编译器你有一个 Group
类构造函数,其实例与工厂函数版本的相同通用交集。例如:
class _Group {
items: GroupItem[];
constructor(items: GroupItemInGroup) {
this.items = [];
Object.keys(items).forEach((k) => {
this.items.push(items[k]);
(this as Group<any>)[k] = items[k];
});
}
apply(): void {
throw new Error('Method not implemented.');
}
}
type Group<K extends PropertyKey> = _Group & Record<K, GroupItem>;
const Group = _Group as new <K extends PropertyKey>(
items: Record<K, GroupItem>) => Group<K>
在 _Group
的实现中,编译器不期望 this
具有任意的 string
键,因此我们需要一种断言来使 this[k]
起作用。然后,Group
值与 _Group
相同,但我们已经断言其类型是生成 Group<K>
实例的通用构造签名,其中 Group<K>
是 Group
实例类型和 Record<K, GroupItem>
的相关交集。
现在它的行为与你最初想要的一样:
const theGroup = new Group({
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first); // okay
console.log(theGroup.second); // okay
console.log(theGroup.items); // okay
try { theGroup.apply() } catch (e) { console.log(e) } // okay
英文:
Conceptually you want Group
to be generic in the keys of the items
constructor argument, like class Group<K> implements Record<K, GroupItem> {⋯}
(using the Record<K, V>
utility type). But that's not allowed in TypeScript; interfaces and classes require statically known keys, and K
is not statically known. See microsoft/TypeScript#2225 and microsoft/TypeScript#21326.
If you can give up on using a class and write a factory function instead, this is relatively straightforward to write:
function Group<K extends PropertyKey>(items: Record<K, GroupItem>) {
const _items: { [k: string]: GroupItem } = items; // widen
return {
...items,
items: [...Object.values(_items)],
apply(): void {
throw new Error('Method not implemented.');
}
}
}
/* function Group<K extends PropertyKey>(items: Record<K, GroupItem>
): Record<K, GroupItem> & { items: GroupItem[]; apply(): void; }} */
The returned object's type is automatically computed by TypeScript to be the intersection of the pieces of the object literal. Now when you call it (instead of new
) you get the desired behavior:
const theGroup = Group({
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first); // okay
console.log(theGroup.second); // okay
console.log(theGroup.items); // okay
try { theGroup.apply() } catch (e) { console.log(e) } // okay
If you need to use a class then you have to fake it up by writing the closest class you can to what you want, rename it out of the way (e.g., _Group
), and then use type assertions to tell the compiler that you have a Group
class constructor whose instances are the same generic intersection from the factory function version. For example:
class _Group {
items: GroupItem[];
constructor(items: GroupItemInGroup) {
this.items = [];
Object.keys(items).forEach((k) => {
this.items.push(items[k]);
(this as Group<any>)[k] = items[k];
});
}
apply(): void {
throw new Error('Method not implemented.');
}
}
type Group<K extends PropertyKey> = _Group & Record<K, GroupItem>;
const Group = _Group as new <K extends PropertyKey>(
items: Record<K, GroupItem>) => Group<K>;
Inside the implementation of _Group
the compiler doesn't expect this
to have arbitrary string
keys so we need an assertion to make this[k]
work. And then the Group
value is just the same as _Group
, but we've asserted that its type is of a generic construct signature producing a Group<K>
instance, where Group<K>
is the relevant intersection of Group
instance types and Record<K, GroupItem>
.
Now it behaves as you wanted initially:
const theGroup = new Group({
first: new GroupItem(),
second: new GroupItem(),
});
console.log(theGroup.first); // okay
console.log(theGroup.second); // okay
console.log(theGroup.items); // okay
try { theGroup.apply() } catch (e) { console.log(e) } // okay
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论