如何将属性类型设置为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,这很容易。

第三条评论后的UPD

至少,也许有可能得到这个:

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,原则上是不可观察的。这很难找到一个权威的来源,但是如果你参考一下Microsoft/TypeScript#3060和Microsoft/TypeScript#3628的注释,你会看到一些相关的信息和推理。

TypeScript的类型系统是结构化的,而不是标称的。这意味着如果两个类型AB具有相同的结构,则它们是相同的类型。它们是否有不同的名称声明并不重要。例如:
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的类型被推断为匿名类型。然而,当您将c传递给acceptA()时,编译器没有问题,也不会关心您是否将B类型的任意值传递给acceptA()。对于编译器来说,ABtypeof C是相同的类型。它们的显示方式可能不同,但它们不是不同的类型。

因为它们是相同的类型,那么原则上应该不可能编写任何类型函数type F<T> = ...,使F<A>F<B>F<typeof c>产生任何不同的类型。如果您希望DynamicPropertyName<A>生成包含文字类型"A"的类型,但DynamicPropertyName<B>生成包含"B"而不是"A"的类型,那么您正在寻找的将违反结构类型,因此是不可能的。

嗯,嗯…有时编译器会生成违反结构类型的类型,但这些都是边缘情况,您不能依赖它们。当某些东西应该是不可观察的,而您设法观察到它时,您实际上是在欺骗编译器暴露一些内部实现细节,这些细节可能会发生变化。我正在考虑将联合转换为元组,或者尝试推断未使用的类型参数的值。有时候你可以从编译器那里得到隐藏的信息,但是你得到的是未定义的行为,而不是问题的解决方案。


现在类名有点不同,因为类构造函数有时在运行时确实有name属性。但是在运行时,你不能指望这个属性是任何特定的东西。

有microsoft/TypeScript#43325要求classes的更多强类型name属性。如果这是在适当的地方,你可以这样做class Foo

但是,即使你能100%保证Foo.name在运行时是"Foo", TypeScript中也有理由不在类型级别公开这一点。如果class Foo有一个类型为"Foo"的静态name属性,那么class Baz extends Foo {}应该有一个类型为"Baz"的静态name属性,对吗?但是,typeof Baz extends typeof Foo将为假,如果类实例类型具有强类型constructor属性,则Baz extends Foo将为假。如果写入class Baz extends Foo {}会导致类型Baz没有扩展Foo,这将是令人不快的。所有这些都用于强类型名称属性。所以这是不可能发生的。


好的,所以。编译器并不关心Foo是否被命名为FooBar是否被命名为Bar。为了更好地使用TypeScript,你也不应该在意这些。您似乎想要这样做,这可能表明您有XY问题。如果您可以让编译器为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链接到代码

最新更新