Emil Walser
Emil Walser

Reputation: 652

Extract "defined" type from property in TypeScript at runtime

What I want to do

I'm currently looping over an object's keys and transferring the values to another object.

interface From {
    [key: string]: string;
}

let from: From = {
    prop1: "foo",
    prop2: "23",
};

interface To {
    [key: string]: string | number | boolean | undefined;
    prop1: string;
    prop2: number;
    prop3: boolean;
}

let to: To = {} as To;

const initEnv = async () => {
    const keys: string[] = Object.keys(from);

    for (let i = 0; i < keys.length; i++) {
        let key: string = keys[i];

        let keyType = typeof (key as keyof To); // This only returns "string". Which kind of makes sense to me

        let keyType = typeof keyof key; // SyntaxError: ',' expected

        let keyType: typeof to[key]; // Error: 'key' refers to a value, but is being used as a type

        to[key] = from[key];
    }
};

I would want to be able to, say switch the value, so I don't want to just extract the type of the key. I want to assign it to a variable for use in the code, thus at runtime, as a string for instance.
So I think things like this wouldn't work.

let keyType2: typeof env[key]; // Error: 'key' refers to a value, but is being used as a type

Maybe, but the question is then; what do I assign to this variable?

The reason for all this, is that I want to convert the from variables to the correct type before, assigning them to the to object.

So yeah, basically, my question is how I would extract the type (as a string, at runtime, dynamically) from the key. Or is it even possible in the first place? And if it isn't why not? I like understanding things.

Thanks for putting up with my bad english, as well.

Upvotes: 0

Views: 2525

Answers (1)

jcalz
jcalz

Reputation: 328292

There are no interfaces at runtime; TypeScript's type system is erased from the emitted JavaScript. Your from and to values will be evaluated like this at runtime:

let from = {
    prop1: "foo",
    prop2: "23",
};
let to = {};

There's no From or To, and no way to use To to figure out how to coerce from's properties into the right types. The type system has no runtime effects.


The usefulness of TypeScript's type system comes from describing what will happen at runtime and not from affecting things at runtime. Imagine how you would have to write your code in pure JavaScript, and then give types to that code. Here's one way I might do it. Instead of a To interface, let's make a To object whose properties are functions that coerce inputs to other types:

const To = {
  prop1: String,
  prop2: Number,
  prop3: Boolean
}

This is enough information to proceed at runtime.


Now, if you were going to build to manually, the compiler would be able to understand that the resulting value has a prop1 property of type string and a prop2 property of type number:

const toManual = { prop1: To.prop1(from.prop1), prop2: To.prop2(from.prop2) };
/* const toManual: { prop1: string; prop2: number; } */

But you don't want to do it manually; you'd like to write a loop that walks through the keys of from and uses To to produce properties of to. This is harder for the compiler to understand, but with judicious use of type assertions and type annotations you can write an objMap function that works programmatically:

function objMap<T, F extends { [K in keyof T]: (arg: T[K]) => any }>(
  obj: T, fMap: F) {
  const ret = {} as { [K in keyof T]: ReturnType<F[K]> };
  const fM: { [K in keyof T]: (arg: T[K]) => ReturnType<F[K]> } = fMap;
  (Object.keys(obj) as Array<keyof T>).forEach(<K extends keyof T>(k: K) => {
    ret[k] = fM[k](obj[k]);
  })
  return ret;
}

The objMap function takes an object obj and a mapping object fMap which has at least all the same keys as obj and whose properties are functions that map obj's properties. The return type of objMap is an object whose properties are all the returned values of the fMap function for each property in obj. The actual work is being done by ret[k] = fM[k](obj[k]);. It's the programmatic equivalent of ret.prop1 = To.prop1(from.prop1); and ret.prop2 = To.prop2(from.prop2).

Let's see if it works:

const to = objMap(from, To);
/* const to: { prop1: string; prop2: number; } */

console.log(JSON.stringify(to));
// {"prop1":"foo","prop2":23}

That looks correct; the type of to is inferred by the compiler to be {prop1: string, prop2: number}, and the actual value of to is computed at runtime to be {prop1: "foo", prop2: 23}.


Playground link to code

Upvotes: 1

Related Questions