Skip to content
Advertisement

Best way to get dynamic type definition in typescript from a configuration object

I have a system in normal javascript where a model can be defined with types as a method override and are casted when the data comes in, but want to get the typescript benefits purely on the assignment (not the instantiation, or at least not as important).

the typescript should be a product of a configuration dictionary / plain object. is there a way to define the following pattern dynamically? I did see some examples of defining a dict but i want to do it for any model not specifically one (which would still require the code duplication)

i saw maybe can make a typescript plugin or do interface code generation. after looking around i didnt see any similar answers that did this. just wondering if anyone has any thoughts on best way to accomplish or if its just not possible.

Dynamically produce the typings from attributes?

User uses model . model is our base model class that used public attributes with types to cast and set things like readonly or any. need to add typescript but basically repeats myself from what should be easily derived from the source of truth definePublicAttributes

JavaScript

for a complete minimum reproducable example see a contrived simplified version of what im doing

(p.s. please note outside of initial cast the model system is used throughout the lifecycle of the app for other things like changes but ommitted for simplicity)

Minimum reproducable example

Two models (Pet and Person) extends base Model, instantiated from a nested plain object and model type definitions. i just want typescript to prevent bad type assignments and docs while editing, but without maybe needing to explictly write the type definitions, instead, getting it from a property / method / source like the get publicAttributes().

JavaScript

Advertisement

Answer

The only way I can envision this working is if you write a generic class factory function (let’s call it ModelFromAttributes()) that takes the publicAttributes value as its input of generic type T and produces an appropriate subclass of Model as its output, whose type is a determined by T. The relationship between T (the type of publicAttributes) and the output class’s instance type is somewhat complex, so we will need to define a helper type (let’s also call it type ModelFromAttributes<T>) to capture that.

So we will need something like:

JavaScript

And we will use it like

JavaScript

So ModelFromAttributes({...}) takes an input of type T and produces an output with a construct signature whose instance type is ModelFromAttributes<T>. So what type is that?

Well, we want a ModelFromAttributes<T> to have a publicAttributes property of type T, and it should also be a Model since it will inherit from Model, and it should also have a bunch of optional properties whose names and types are determined by T in some way. So it should look like:

JavaScript

where we are using the Partial<X> helper type to turn all the properties into optional ones, and where MFA<T> is an as-yet undefined type that needs to represent the conversion of the type T of the attributes to the type MFA<T> of the corresponding class properties. The intersection operator (&) can be read as “and”; a ModelFromAttributes<T> is an object with a publicAttributes property of type T, and it is a Model, and it is an all-optional version of MFA<T>, whatever that is.


Okay, now we have to define MFA<T>. Here’s one possible definition:

JavaScript

This is a conditional type that checks T and evaluates to different things based on the check.

So, if T is assignable to typeof Date (using the type-level typeof type operator to describe the type of the value named Date), then MFA<T> should be Date. If T is the type of the String constructor, then MFA<T> should be string. And the same for number and boolean.

If T is some other class constructor type, then MFA<T> should be the instance type of that class (using conditional type inference to extract that class type).

If T is some other function type, then MFA<T> should start with the return type of that function, but because the return type might itself be something like the String or Date constructor, we need to apply MFA<> to that type recursively. That is, MFA<T> is a recursive conditional type.

Finally, if T is some other object type, then MFA<T> is a mapped type, for each property in T with key K, we take the property value type T[K] and apply MFA<> to it itself. So this is another recursive step. Also note that mapped types on arrays/tuples produce arrays/tuples so it should automatically work as desired on arrays without extra work.


Before moving on, let’s make sure it acts as expected on your Pet and Person attribute types:

JavaScript

That looks good so far. We don’t have a Pet class constructor yet so let’s just describe one with a hand-crafted interface that should be the same thing:

JavaScript

Also looks good.


Okay, all we have left to do is actually implement ModelFromAttributes(). Like this:

JavaScript

I used type assertions in there to suppress compiler errors. The compiler has no good way to understand that the class expression actually constructs instances of ModelFromAttributes<T>; a generic conditional type like ModelFromAttributes<T> where T is not specified yet is essentially opaque to the compiler. Generally speaking the compiler won’t be able to look at a class declaration or expression and infer dynamic property keys; classes generate interface types, and interfaces can only have statically known property names. So something like a type assertion will be necessary.

All that means is we need to be careful that our implementation does what we say it does.


So let’s try it:

JavaScript

That all compiles without error. Let’s use IntelliSense to examine the types of bill‘s properties:

JavaScript

Looks good! The properties of bill are all the properties of Model, plus a strongly-typed publicAttributes type, plus all optional properties corresponding to MFA<>.

Playground link to code

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement