如何将 属性 类型设置为 TypeScript 中类型名称的文本表示?

How to set a property type to textable representation of Type name in TypeScript?

说,我有这些 class 和界面:

class Foo {}
interface Bar {}

我想创建一个 type 具有指定类型的 属性 名称:

type DynamicPropertyName<T> = ... <-- ???

那我想这样用:

type WithPropFoo = DynamicPropertyName<Foo>; // desire: {Foo: any}
type WithPropBar = DynamicPropertyName<Bar>; // desire: {Bar: any}

而不是 any,我将指定我的类型,这很简单。

第三条评论后更新

至少,也许可以得到这个:

type WithPropFoo = DynamicPropertyName<Foo>; // desire: {type: 'Foo'}
type WithPropBar = DynamicPropertyName<Bar>; // desire: {type: 'Bar'}

const foo: WithPropFoo = {type: 'Foo'};   // ok
const foo: WithPropFoo = {type: 'Hello'}; // error

const foo: WithPropBar = {type: 'Bar'};   // ok
const foo: WithPropBar = {type: 'Hello'}; // error

抱歉,这在 TypeScript 中是不可能的。


Bar这样的接口名称原则上是不可观察的。很难找到这方面的权威来源,但如果您查阅 this comment on Microsoft/TypeScript#3060 and maybe this comment on Microsoft/TypeScript#3628,您会看到一些相关信息和推理。

TypeScript 的类型系统是 structural and not nominal。这意味着如果两个类型 AB 具有相同的 结构 ,则它们是相同的 type。它们是否具有不同的名称声明并不重要。例如:

interface A {x: string}
interface B {x: string}
const c = {x: "hello"};
// const c: { x: string; } 

function acceptA(a: A) {}
acceptA(c); // okay

function acceptB(b: B) {
  acceptA(b); // okay
}    

这里,AB有不同的声明点和不同的名字,c的类型被推断为一个anonymous 类型。然而,当您将 c 传递给 acceptA() 时,编译器没有问题,它也不关心您是否将类型 B 的任意值传递给 acceptA()。就编译器而言,ABtypeof C 是相同的类型。它们可能显示不同,但它们不是不同的类型。

因为它们是相同的类型,所以原则上应该不可能编写任何类型函数 type F<T> = ... 使得 F<A>F<B>F<typeof c> 结果在任何不同的类型。如果您希望 DynamicPropertyName<A> 生成包含 literal type "A" 的类型,但 DynamicPropertyName<B> 生成包含 "B" 而不是 "A" 的类型,那么怎么办您要查找的内容会违反结构类型,因此是不可能的。

嗯,呃... 有时 编译器确实会生成违反结构类型的类型,但这些都是边缘情况,您不能依赖它们。当某些东西应该是不可观察的而你设法观察到它时,你实际上是在欺骗编译器暴露一些内部实现细节,这些细节可能会改变。我特别想到 or 。有时您可以从编译器中提取隐藏信息,但您得到的是未定义的行为,而不是问题的解决方案。


现在 class 名称有点不同,因为 class 构造函数有时 do have a name property at runtime。但是你不能指望 属性 在运行时是任何特定的东西。

microsoft/TypeScript#43325 要求 classes 的更强类型的 name 属性。如果这是适当的,你也许可以为 class Foo.

执行此操作

但即使您可以 100% 保证 Foo.name 在运行时将是 "Foo",TypeScript 也有理由不在类型级别公开它。如果 class Foo 有一个类型为 "Foo" 的静态 name 属性,那么 class Baz extends Foo {} 大概会有一个类型为静态 name 属性 "Baz",对吧?但是 typeof Baz extends typeof Foo 将是错误的,如果 class 实例类型具有强类型 constructor 属性,那么 Baz extends Foo 将是错误的 。如果写 class Baz extends Foo {} 会导致类型 Baz 不扩展 Foo,那将是令人不愉快的。全部用于强类型名称 属性。所以这不太可能发生。


好的,就这样。编译器不关心 Foo 被命名为 FooBar 被命名为 Bar。并且为了更好地使用 TypeScript,您真的也不应该在意。您似乎想要这个的事实可能表明您有 an XY problem。如果可以让编译器为类型 Foo 吐出 "Foo" 并为类型 Bar 吐出 "Bar",则您有一些您认为可以解决的基础用例。由于这是不可能的,您可能想退后一步并再次检查您的用例。

如果您希望字符串文字类型 "Foo""Bar" 出现在某处,您可能希望将它们放在您的代码开头:

class Foo {
  fooProp = 123
  readonly myName = "Foo"
}

interface Bar {
  barProp: string;
  myName: "Bar";
}

此处您明确添加强类型名称作为 myName 属性。执行此操作后,您可以访问这些名称:

type DynamicPropertyName<T extends { myName: string }> = 
  { [K in T["myName"]]: any };

type WithPropFoo = DynamicPropertyName<Foo>; // {Foo: any}
type WithPropBar = DynamicPropertyName<Bar>; // {Bar: any}

您可能觉得这是多余的,但请记住,结构类型意味着它不是。类型名称 Bar 无关紧要,并且 interface Qux {barProp: string myName: "Bar"}Bar 相同,因此如果您关心名称 "Bar",它与接口的声明名称无关.

也许这对您的基础用例不起作用,但关键是您需要通过其他方式实现这些目标。祝你好运!

Playground link to code