Reputation: 30001
Consider the Typescript below. We have two interfaces, both with the same fields that we want to map between. I.e. I want to take an instance of SerialisedModel
and convert it to a Model
. In the example below if I rename fields on SerialisedModel
or Model
, the compiler will not complain and we'll have runtime problems.
What I really want to do is declare Model
to have the same keys as SerialisedModel
, BUT allow different values. E.g. age
is a string
in SerialisedModel
but is a number
in Model
.
I can't seem to figure out how to do this with any Typescript syntax. We've managed to get it somewhat working with Mapped types, but it always ends up with values being any
. Any ideas?
interface SerialisedModel {
name: string;
age: string;
dateOfBirth: string;
}
interface Model {
name: string;
age: number;
dateOfBirth: Date
}
const f: SerialisedModel = // instanciate SerialisedModel;
let b = {} as Model;
Object.keys(f).forEach(key => {
const mapped = f[key] // then do some mapping to convert values
b[key] = mapped;
})
Upvotes: 0
Views: 498
Reputation: 327744
I'm confused about what you're really trying to achieve here. Where are you seeing "it always ends up with values being any
"? I don't see that in my or your code.
If all you are trying to do is have the compiler yell at you if two types have different keys, you can do something like this:
type RequireSameKeys<T extends Record<keyof U, any>, U extends Record<keyof T, any>> = true
type VerifyModelTypes = RequireSameKeys<SerialisedModel, Model>; // okay?
If VerifyModelTypes
has no error, then SerialisedModel
and Model
have exactly the same keys. Let's see:
interface Model {
name: string;
age: number;
dateOfBirth: Date
}
interface SerialisedModel {
name: string;
age: string;
dateOfBrith: string; // Hppay Brithday To Yuo
}
type VerifyModelTypes = RequireSameKeys<SerialisedModel, Model>; // error!
// Property 'dateOfBirth' is missing in type 'SerialisedModel'.
Whoops, let's fix that:
interface SerialisedModel {
name: string;
age: string;
dateOfBirth: string; // fixed
}
type VerifyModelTypes = RequireSameKeys<SerialisedModel, Model>; // okay
It's happy now.
But since SerialisedModel
seems to be a straightforward mapping of Model
, you should indeed be able to use mapped types to define it:
type _SM = { [K in keyof Model]: string };
interface SerialisedModel extends _SM {}; // same as before
and then make a generic processor function to convert between them:
function makeProcessor<T extends Record<keyof T & keyof U, any>,
U extends Record<keyof T & keyof U, any>>(
processors: { [K in keyof T & keyof U]: (x: T[K]) => U[K] }
): (t: T) => U {
return (t: T) => {
const ret = {} as U;
// note Object.keys() returns string[], not (keyof T)[], so we assert
Object.keys(processors).forEach((k: keyof T) => {
ret[k] = processors[k](t[k]);
});
return ret;
}
}
Here's how you'd use it:
const deserialise = makeProcessor<SerialisedModel, Model>({
age: a => +a,
name: n => n,
dateOfBirth: d => new Date(+d)
});
const serialise = makeProcessor<Model, SerialisedModel>({
age: a => "" + a,
name: n => n,
dateOfBirth: d => "" + d.getTime()
})
const model: Model = {
name: "Gabriel Jarret",
age: 48,
dateOfBirth: new Date(0)
}
console.log(model.name); // "Gabriel Jarret"
console.log(model.age); // 48
console.log(model.dateOfBirth); // Date 1970-01-01T00:00:00.000Z
const serialisedModel = serialise(model);
console.log(serialisedModel.name); // "Gabriel Jarret"
console.log(serialisedModel.age); // "48"
console.log(serialisedModel.dateOfBirth); // "0"
const deserialisedModel = deserialise(serialisedModel);
console.log(deserialisedModel.name === model.name); // true
console.log(deserialisedModel.age === model.age); // true
console.log(deserialisedModel.dateOfBirth.getTime() ===
model.dateOfBirth.getTime()); // true
That all looks good to me. Does that help? If you are continuing to see a problem, please include specific code where errors are occurring. Otherwise I'm just making guesses about what you need. Anyway, good luck!
Upvotes: 1