这是这个问题的后续。
我想要达到的目标是: 将多个子类传递给函数是否有一种方法可以返回一个对象,该对象作为具有传递的子类类型的“模式”?这样我就可以保留所有智能感知功能。
下面是我失败的尝试。
class Component {
get name(): string {
return this.constructor.name
}
}
class Direction extends Component {
x: number
y: number
constructor(x: number = 0, y: number = 0) {
super()
this.x = x
this.y = y
}
}
class Rectangle extends Component {
x: number
y: number
w: number
h: number
constructor(x: number, y: number, w: number, h: number) {
super()
this.x = x
this.y = y
this.w = w
this.h = h
}
}
class Entity {
components: Map<string, Component>
constructor(...components: Component[]) {
this.components = new Map()
components.forEach(component => this.add(component))
}
get<C extends Component[]> (...components: { [P in keyof C]: new (...args: any[]) => C[P] }): Record<string, Component> {
return components.reduce(
(accumulator, component) => {
accumulator[component.name.toLocaleLowerCase()] =
this.components.get(component.name)!
return accumulator
},
{} as Record<string, Component>
)
}
add(component: Component) {
this.components.set(component.name, component)
}
}
const player = new Entity(
new Rectangle(100, 100, 100, 100),
new Direction(1, 1)
)
const p_components = player.get(Rectangle, Direction)
console.log(p_components.rectangle)
// Intellisense doens't know p_components properties
// Intellisense infers p_components.rectangle as Components - It should be Rectangle instead
我也在网上搜索过,我发现的最接近的是this。但我不知道如何在我的代码中实现它以及它是否有帮助。
name
属性。相反,它只是 string
,这是事实,但对你想做的事情没有帮助。对于类似的东西有各种请求,例如 microsoft/TypeScript#47203,但它们已被关闭,转而支持 microsoft/TypeScript#1579,等待 JavaScript 实际上提供一致的 nameof
运算符,这可能永远不会发生。
除非这种情况发生变化,如果您希望使用强类型字符串来标识类,则必须自己手动维护它们。例如,您可以为每个类实例提供一个需要实现的
name
属性:
abstract class Component {
abstract readonly name: string;
}
class Direction extends Component {
readonly name = "Direction"
// ✂ ⋯ rest of the class impl ⋯ ✂
}
class Rectangle extends Component {
readonly name = "Rectangle"
// ✂ ⋯ rest of the class impl ⋯ ✂
}
这是您需要执行的手动步骤。但是现在,如果您查看
name
类型的 Rectangle
属性,您会发现它是强类型的:
type RectangleName = Rectangle['name']
// type RectangleName = "Rectangle"
好的,现在你可以编写你的
get()
方法,与你之前的做法非常相似:
get<C extends Component[]>(
...components: { [P in keyof C]: new (...args: any) => C[P] }
): { [T in C[number] as Lowercase<T['name']>]: T } {
return components.reduce(
(accumulator, component) => {
accumulator[component.name.toLowerCase()] =
this.components.get(component.name)!
return accumulator
},
{} as any
)
}
映射数组/元组类型,它约束到
Component
实例类型的数组。因此,如果您调用 get(Rectangle, Direction)
,那么 C
将是元组类型 [Rectangle, Direction]
。
输出类型是您关心的类型......在本例中是
{ [T in C[number] as Lowercase<T['name']>]: T }
。这是基于类型 C[number]
的键重映射类型,它是
C
元素类型的 union。 (如果
C
是一个数组类型,并且您使用 number
类型的键对其进行索引,那么您将获得该数组的一个元素。因此,索引访问类型 C[number]
对应于以下元素类型数组。有关更多信息,请参阅数组类型的 Typescript 接口。)
对于
T
中的每个组件类型 C[number]
,我们希望输出中的键为 Lowercase<T['name']>
。也就是说,我们使用 T
对 name
进行索引以获取强类型名称,然后使用 LowerCase<T>
实用程序类型 将其转换为小写...以对应于component.name.toLowerCase()
的行为。请注意,我将其从 toLocaleLowerCase()
更改为与语言环境无关的 toLowerCase()
。 TypeScript 完全不知道运行时将使用什么语言环境,除非 you 知道,否则你不想犯这样的错误:假设语言环境的小写规则会将 "Rectangle"
变成 "rectangle"
。如果您有一个名为 "Index"
的组件,并且运行时区域设置恰好是土耳其语,那么它将变成 "ındex"
而不是 "index"
,您将会遇到麻烦。
同样,对于
T
中的每个组件类型 C[number]
,键将是 name
属性的与区域设置无关的小写版本。该值将只是T
。因此,{ [T in C[number] as Lowercase<T['name']>]: T }
。
让我们测试一下:
const p_components = player.get(Rectangle, Direction);
// const p_components: {
// direction: Direction;
// rectangle: Rectangle;
// }
这看起来就是你想要的。
请注意,这只能帮助您输入,而不能真正帮助您实现。您可以使用从未被
player.get()
编辑过的 Component
子类来调用 add()
,并在运行时当类型显示它将出现时获取 undefined
。您可以将实现修复为更通用,或者将打字考虑到undefined
,但我认为这超出了所提出问题的范围。