'How to map a function over all properties of an object while keeping type safety?

I have these two TypeScript definitions:

interface Snapshot {
    guild: Guild;
    channels: ChannelsDataModel;
    roles: RolesDataModel;
    emojis: EmojisDataModel;
    stickers: StickersDataModel;
    permissionOverwrites: PermissionOverwritesDataModel;
    bans: BansDataModel;
    metadata: SnapshotMetadata;
}

type SerializedSnapshot = { [Property in keyof Snapshot]: string };

Basically what that means is that, when serializing Snapshot, all properties get converted to a string using JSON.stringify() over that property and SerializedSnapshot describes just that.

However, when creating the new serialized object, I would have to write this repetitive piece of code:

function serializeSnapshot(snapshot: Snapshot): SerializedSnapshot {
    // I already removed a bit of code repetition by creating this inner function, but that's not much.
    function stringify(value: any): string {
        return JSON.stringify(value, null, 2);
    }

    return {
        guild: stringify(snapshot.guild),
        channels: stringify(snapshot.channels),
        roles: stringify(snapshot.roles),
        emojis: stringify(snapshot.emojis),
        stickers: stringify(snapshot.stickers),
        permissionOverwrites: stringify(snapshot.permissionOverwrites),
        bans: stringify(snapshot.bans),
        metadata: stringify(snapshot.metadata),
    };
}

What bothers me is that the object creation forces me to repeat stringify() for each property of the original object. Being a fan of the map() function, I wish there would be a way to "map" one property of an object to another object that (clearly from the example) has the same property names (or keys?) but they differ in type. Their type would be the return type of the hypothetical property mapping function. Essentially, a way to make TypeScript understand that the resulting object has the same property names with a different value, defined by the map function.

Is something like this doable in TypeScript's type system?



Solution 1:[1]

Is something like this doable in TypeScript's type system?

There are two answers to that:

  • Yes, it's what's in your question as SerializedSnapshot.
  • No, because you're talking about a runtime process of changing values from non-strings to strings.

So the type part of — the TypeScript type system part — you're already doing. But the runtime part still needs doing.

You can do it by using Object.entries to get an array of [key, value] tuples for the object, map to convert the values in those to strings, and Object.fromEntries to build the new object if you don't mind the intermediate arrays:

return Object.fromEntries(
    Object.entries(snapshot).map(([key, value]) => [key, stringify(value)])
) as SerializedSnapshot;

Playground link

I can't say I like the type assertion it requires, but in very well-controlled, limited situations like your serializeSnapshot, a type assertion may be okay.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 T.J. Crowder