1

Is it possible to define a property name based on the generic type passed?

type Foo<T> = {
  // magic goes here
  someProp: boolean
};

enum MyEnum {
  red = 'red',
  blue= 'blue',
}

const d: Foo<MyEnum.blue> = null;
d.someProp
d.blue // Property 'blue' does not exist on type 'Foo<MyEnum.blue>'.ts(2339)

Been looking all over the docs, but I can't figure it out.

3
  • 1
    Are you just looking for something like the Record utility type as in this? Commented Apr 8, 2021 at 16:37
  • Exactly that @jcalz, thank you!. is there a way to do it with an interface instead of a type? Commented Apr 8, 2021 at 18:23
  • No, an interface must have statically known keys. You can write interface XYZ extends Foo<MyEnum.blue> {} but you cannot write interface XYZ<T extends PropertyKey> extends Foo<T> {}. Commented Apr 8, 2021 at 19:06

1 Answer 1

1

You can use a mapped type to generate an object type on the fly given the types of its keys and associated values. For example, the type {[P in "x" | "y"]: [P]} produces the type {x: ["x"], y: ["y"]}. For the case where the value type does not depend on the key, you can use the built in Record<K, V> utility type, described in more detail here. So Record<"x" | "y", number> is equivalent to {x: number, y: number}.

In your case, you want Foo<T> to have the key T, which means we need to constrain T to just keylike types (that is string | number | symbol, or you can use the built in PropertyKey alias). In such cases, K is a more conventional parameter name than T. Let me start over:

You want Foo<K extends PropertyKey> to have K as a key. I'm not sure what value type you want at that key; for now I'll just use any, the ultimate 🤷‍♂️ type. And you walso want Foo<K> to have a boolean property at the "someProp" key. So it should be both Record<K, any>, and a {someProp: boolean}. That implies you want it to be the intersection of those:

type Foo<K extends PropertyKey> = {
  someProp: boolean
} & Record<K, any>

And you can verify that this works:

const d: Foo<MyEnum.blue> = {
  blue: 123,
  someProp: true
}

Note that you cannot make Foo an interface. In both class and interface types, the compiler needs to know the keys statically, in advance of usage:

interface Oops<K extends PropertyKey>
  extends Foo<K> { } // error!
// -----> ~~~~~~
// An interface can only extend an object type or 
// intersection of object types with statically known members.

For a particular specification of K, though, you can make an interface:

interface FooBlue extends Foo<MyEnum.blue> { } // okay

const e: FooBlue = {
  someProp: true,
  blue: new Date()
}

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.