为什么类型 {[x: string]: this} 不等同于 TypeScript 类成员声明中的 Record<string, this>?

Why is the type {[x: string]: this} not equivalent to Record<string, this> in TypeScript class member declarations?

提问人:cpcallen 提问时间:9/29/2023 最后编辑:cpcallen 更新时间:9/29/2023 访问量:59

问:

背景

上周,我问了一个关于如何声明一个类成员的问题,该类成员是一个函数,其参数与类是协变的,对于它来说,使用多态这种类型是一个完美的解决方案。

我的实际代码有一个带有成员的类,该成员是此类函数的字典。在调整解决方案时,遇到了意外的类型错误。

问题

给定以下类声明

class C {
    public dict1: {[x: string]: this} = {};    // Error: A 'this' type is available only in a non-static member of a class or interface.
    public dict2: Record<string, this> = {};   // OK
    public dict3: {[P in string]: this} = {};  // OK?!
}

为什么 的声明 ,使用索引签名,会引起错误dict1

“this”类型仅在类或接口的非静态成员中可用

但是看似等效的声明(使用实用程序类型)和(使用基于 Record<> 定义的不同索引签名)不会引起相同的错误?dict2Record<>dict3

language-lawyer typescript-generics free-variable

评论

0赞 jcalz 9/29/2023
编辑以提出一个问题,以避免因需要重点而关闭。你的 1 和 2 可能被问到一个问题的一部分,但 3 本质上是不同的。我们应该关注哪一个?
1赞 jcalz 9/29/2023
如果你选择 1&2,答案是,在它所指的类型中是被禁止的(在 中,确实指的是或 ?),并且任何清除这一点的间接信息都将被允许。如果选择 3,那只是一个映射类型,并且有据可查。我很乐意将其中一个充实到答案中,但不是两个答案,所以请在有机会时编辑问题和/或让我知道如何进行。thisinterface X {a: {b: this}}thisXX["a"]
0赞 cpcallen 9/29/2023
@jcalz:谢谢你的建议。我确实至少有两个问题,但我重写了它,只关注其中一个,尽管仍然引用了所有三个例子。(对于后代:您在评论中有用地回答的原始问题 3 是“(这里和记录的定义<>是怎么回事?P
1赞 jcalz 9/29/2023
好的,当我有机会时,我会为当前版本的问题写一个答案;可能至少从现在起三个小时。

答:

1赞 jcalz 9/29/2023 #1

请参阅 microsoft/TypeScript#47868 以获取此问题大部分的规范答案。在 microsoft/TypeScript#4910 中实现的多态 this 类型通常是指在提及该值的范围内 this的类型。对于像这样的类型

interface Foo { a: this }

this指的是 的某个子类型,所以你可以像这样实现它FooFoo

class Bar implements Foo { a = this; }
class Baz extends Bar { z = "abc" }
const baz = new Baz();
console.log(baz.a.a.a.z.toUpperCase()); // "ABC"

所以 、 、 等都是 类型 。真棒。bazbaz.abaz.a.aBaz

但是对于像这样的类型

interface Qux { a: { b: this } } // error!
// -------------------> ~~~~
// only in an interface property or a class instance member 

指的是什么?相关范围是什么?人们可能会期望它指的是外部范围,因此它是(或一个子类型),但也许它只是指内部范围,因此它是(或子类型)。这是模棱两可的。QuxQux["a"]

TypeScript 通过只允许在接口属性或类实例成员中使用,而不干预范围来避免歧义。这就是为什么上面是一个错误。this

为了指定你指的是哪一个,你必须使用一些间接的,可能是泛型。如果需要外部范围,可以执行以下操作:

interface B<T> { b: T }
interface Qux { a: B<this> }

现在只能引用 的子类型。你可以用一个类来实现它,比如thisQux

class Quux implements Qux {
    a = { b: this }
    z = "abc";
}
const q = new Quux();
console.log(q.a.b.a.b.z.toUpperCase()) // "ABC"

所以 、 、 等 都是 类型 。qq.a.bq.a.b.a.bQuux

如果需要内部范围,可以执行以下操作:

interface B { b: this }
interface Qux { a: B }

现在只能引用 的子类型。您可以像这样实现它thisB

class Quux implements Qux {
    a = { get b() { return this }, z: "abc" };
}
const q = new Quux();
console.log(q.a.b.b.b.z.toUpperCase()) // "ABC"

在这里,我使用了吸气剂,但如果你愿意,你可以用另一个类来做。反正现在 、 、 等 都是 类型 。q.aq.a.bq.a.b.bQuux["a"]


对于您的示例,

class C {
    dict: { [x: string]: this } = {};
}

您有一个索引签名,其行为类似于对象类型中的一系列键。索引签名可以与对象类型中的其他属性(例如 )并列。{[k: string]: string; foo: "bar"}

并且和 .是否指 的实例或仅指 的属性?看起来你打算前者。因此,我们需要某种间接性。你可以这样做,就像{ [x: string]: this }{ a: this }thisCdictC

type Dict<T> = { [x: string]: T };
class C {
    dict: Dict<this> = {}; // okay
}

但是,当然,您不必编写自己的类型。Record 实用程序类型可以通过这种方式投入使用,其中本质上是相同的:DictRecord<string, T>

class C {
    dict: Record<string, this> = {}; // okay
}

最后,如果你定义一个内联的映射类型,比如

class C {
    dict: { [K in string]: this } = {}; // okay
}

它也有效,因为显然映射类型不会引入自己的范围。请注意,虽然映射类型的语法看起来与索引签名的语法相似,但它们并不相同。(问题将其称为索引签名,但它不是索引签名。索引签名可以被认为是对象类型的一个部分(例如,其他属性和签名可以与它一起出现在对象类型中),而映射类型是它自己的独立事物(例如,大括号是映射类型的一部分;你不能添加其他属性)。有关更多信息,请参阅另一个问题的答案


所以你去吧。根据编译器,您只能在范围不明确的地方使用多态。不能在嵌套对象属性或索引签名中执行此操作,但可以使用间接、泛型和明显映射的类型执行此操作。this

Playground 代码链接