Typescript - 如何从动态创建的对象推断正确的子类类型

问题描述 投票:0回答:1

这是这个问题的后续。

我想要达到的目标是: 将多个子类传递给函数是否有一种方法可以返回一个对象,该对象作为具有传递的子类类型的“模式”?这样我就可以保留所有智能感知功能。

下面是我失败的尝试。

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。但我不知道如何在我的代码中实现它以及它是否有帮助。

typescript templates variadic-templates
1个回答
1
投票

TypeScript 不会将字符串文字类型赋予类构造函数的

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
    )
}

输入类型是相同的,是泛型C上的

映射数组/元组类型
,它约束
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
,但我认为这超出了所提出问题的范围。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.