How to map a concrete class to another type without nested ternary?

Viewed 64

Suppose I have a class, say, BasicModal and one type describing BasicModal's configurations, type BasicModalConfig = {} for instance, and I also have a function, that takes the class and its settings a parameters, let's assume its signature is function openModal(component, configs);. The goal is to infer the component's type (or class name if useful/possible) and use it in config's type as a type parameter and set the configurations value accordingly, in other words, mapped types, but without the nested ternary madness.

Example of the desired result:

// This works

interface BasicModalConfig {
  autoClose: boolean;
};

class BasicModal {}

// This would get out of hand very fast
type TypeResolver<K> = K extends BasicModal ? BasicModalConfig : never;

function openModal<T>(component: T, configs: TypeResolver<T>) {}

openModal(BasicModal, {
  autoClose: true // this should give me hints/autocompletion
});

// This doesn't work
interface Resolver {
  BasicModal: BasicModalConfig;
}
//or (doesn't compile)
//type Resolver = {
//  [x: typeof BasicModal]: BasicModalConfig;
//};

function openModal2<T>(component: T, configs: Resolver[T extends keyof Resolver ? T : never]
) {}

openModal2(BasicModal, {
  autoClose: true // this should give me hints/autocompletion, but config is "never"
});

I searched for something like "nameof" from C#, in Typescript repository, to use it as a key value, but it's still in discussion: https://github.com/microsoft/TypeScript/issues/1579

As of typescript version 4.6, is it possible to even achieve this?

Here's a Typescript playground.

1 Answers

I can see two solutions, depending on how coupled you want the config and model to be.

You could add a field to the model that is the same type as the config and use conditional types to extract the config type:

type TypeResolver<K> = K extends { __config?: infer U } ? U : never;

function openModal<T>(component: T, configs: TypeResolver<T>) {}

openModal(BasicModal, {
  autoClose: true // this should give me hints/autocompletion
});

Playground Link

You could also have a mapping between types, but since as you point out there is no nameof in TS, you can use an object type for the value and extract from the values in the interface:

type TypeResolver<K> = Extract<TypeResolverMap[keyof TypeResolverMap], { model: K }>['config']

interface TypeResolverMap {
  BasicModal: { model: typeof BasicModal; config: BasicModalConfig }
}

Playground Link

Or you could use a tuple for the mapping instead of an interface (although interfaces are open ended so you can add models from other files):

type TypeResolver<K> = Extract<TypeResolverMap[number], [K, any]>[1]

type TypeResolverMap = [
  [typeof BasicModal, BasicModalConfig]
]

Playground Link

Or a mix between an interface and tuples:

type TypeResolver<K> = Extract<TypeResolverMap[keyof TypeResolverMap], [K, any]>[1]

interface TypeResolverMap {
  BasicModal: [typeof BasicModal, BasicModalConfig]
}

Playground Link

Related