Say, I have these class & interface:
class Foo {}
interface Bar {}
I want to create such a type
that has a property name of a specified type:
type DynamicPropertyName<T> = ... <-- ???
Then I want to use it like so:
type WithPropFoo = DynamicPropertyName<Foo>; // desire: {Foo: any}
type WithPropBar = DynamicPropertyName<Bar>; // desire: {Bar: any}
Instead of any
, I will specify my type, that's easy.
At least, maybe it is possible to get this:
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
Sorry, but this is impossible in TypeScript.
Interface names like Bar
are, in principle, unobservable. It's hard to find an authortitative source for this, but if you consult this comment on Microsoft/TypeScript#3060 and maybe this comment on Microsoft/TypeScript#3628, you'll see some related information and reasoning.
TypeScript's type system is structural and not nominal. That means if two types A
and B
have the same structure, then they are the same type. It doesn't matter if they have different names or declarations. For example:
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
}
Here, A
and B
have different declaration sites and different names, and c
's type is inferred to be an anonymous type. Yet the compiler has no problems when you pass c
into acceptA()
, nor does it care if you pass an arbitrary value of type B
into acceptA()
. As far as the compiler is concerned, A
, B
, and typeof C
are identical types. They might be displayed differently, but they are not different types.
Because they are the same types, then it should not be possible in principle to write any type function type F<T> = ...
such that F<A>
and F<B>
and F<typeof c>
result in any different type. If you want DynamicPropertyName<A>
to produce a type containing the literal type "A"
but DynamicPropertyName<B>
to produce a type containing "B"
instead of "A"
, then what you are looking for would violate structural typing and is therefore impossible.
Well, uh... it sometimes happens that the compiler does produce types that violate structural typing, but these are edge cases and you can't rely on them. When something is supposed to be unobservable and you manage to observe it, you are essentially tricking the compiler into exposing some internal implementation detail, which might possibly change. I'm thinking of converting unions to tuples or trying to infer values for unused type parameters in particular. You can sometimes tease hidden information from the compiler, but what you get is undefined behavior, not a solution to a problem.
Now class names are a little different, since class constructors sometimes do have a name
property at runtime. But you can't rely on that property being anything in particular at runtime.
There's microsoft/TypeScript#43325 asking for more strongly typed name
properties of class
es. If this were in place you might be able to do this for class Foo
.
But even if you could 100% guarantee that Foo.name
at runtime would be "Foo"
, there are reasons in TypeScript not to expose this at the type level. If class Foo
had a static name
property of type "Foo"
, then class Baz extends Foo {}
would presumably have a static name
property of type "Baz"
, right? But then typeof Baz extends typeof Foo
would be false, and if class instance types had strongly typed constructor
properties, then Baz extends Foo
would be false. It would be unpleasant if writing class Baz extends Foo {}
would result in a type Baz
that does not extend Foo
. All for a strongly-typed name property. So it's not likely ever to happen.
Okay, so. The compiler doesn't care that Foo
is named Foo
or that Bar
is named Bar
. And to play nice with TypeScript, you really shouldn't care either. The fact that you seem to want this might indicate that you have an XY problem. You have some underlying use case that you thought might be solved if you can get the compiler to spit out "Foo"
for type Foo
and "Bar"
for type Bar
. Since this is not possible, you might want to step back and examine your use case again.
If you want the string literal types "Foo"
and "Bar"
to appear somewhere, you might want to put them in your code to begin with:
class Foo {
fooProp = 123
readonly myName = "Foo"
}
interface Bar {
barProp: string;
myName: "Bar";
}
Here you are explicitly adding strongly-typed names as a myName
property. Once you do this, you can access these names:
type DynamicPropertyName<T extends { myName: string }> =
{ [K in T["myName"]]: any };
type WithPropFoo = DynamicPropertyName<Foo>; // {Foo: any}
type WithPropBar = DynamicPropertyName<Bar>; // {Bar: any}
You might feel that this is redundant, but remember that structural typing means it's not. The type name Bar
is irrelevant, and interface Qux {barProp: string myName: "Bar"}
is identical to Bar
, so if you care about having the name "Bar"
it has nothing to do the declared name of the interface.
Maybe this doesn't work for your underlying use case, but the point is that you will need to achieve those goals some other way. Good luck!