How to keep type safety and (if possible) prevent duplication when you are interacting with JS that requires a lot of string concatenation

Hello.
I’m in a position where I have to interact with DynamoDB. Almost every query requires a great amount of string concatenations. Creating items is not bad, just use a proper serialization library and it supports adding JS objects out of the box.

The problem is with all the other queries, specially updates. Updates first require a lot of optionals, so I need to either create an identical type with all the fields optional or a function that takes a lot of optional values and then performs optional checks on them. This is not only tedious, but also very error prone, because everytime I want to add a new field I need to make sure that I add it to all the proper strings in all the proper places (usually to the name maps, to the values and to the list of fields to update). On top of that the API is very forgiving, and it doesn’t give you much hints if you makes mistakes.

I thought that I could use deriving abstract to at least define a type with all the optional fields. This has the advantage that the generated JS object does not contain the missing fields, so I can happily do some string concatenation of object keys and values. This will be good enough except for the detail that some fields need special serialisation, so my iteration on the generic values is not valid because some types will be stored as weird JS values, and that is not acceptable.

Here is my solution that is not valid for the reason I already commented. If someone can propose a solution that is type safe, even if it is a bit more tedious, I will be happy. Not sure if GADTs could help here

[@bs.deriving abstract]
type t_update = {
  [@bs.optional]
  dimensions: Dimensions.t,
  [@bs.optional]
  point: Point.t,
  [@bs.optional]
  rounded: Rounded.t,
};

external entries: 'a => array((string, 'b)) = "Object.entries";

let update = (~storyID, ~layerID, ~sectionID, ~style: t_update) => {
  let asObj = Obj.magic(style);
  let updateExpressionArray =
    asObj->Js.Obj.keys->Js.Array2.map(key => {j|#$key = :$key|j});
  let expressionAttributeNamesArray =
    asObj->Js.Obj.keys->Js.Array2.map(key => ("#" ++ key, key));
  let expressionAttributeValuesArray =
    asObj
    ->entries
    ->Js.Array2.map(((key, value)) => (":" ++ key, value));

  docClient
  ->DocumentClient.update(
      {
        DocumentClient.updateOptions(
          ~tableName,
          ~key=Key.make(~storyID, ~layerID, ~sectionID)->Key.t_encode,
          ~updateExpression=
            "SET " ++ updateExpressionArray->Js.Array2.joinWith(","),
          ~conditionExpression="#PK = :PK and #SK = :SK",
          ~expressionAttributeNames=
            Js.Dict.fromList(
              [("#PK", "PK"), ("#SK", "SK")]
              ->List.concat(expressionAttributeNamesArray->List.fromArray),
            ),
          ~expressionAttributeValues=
            Obj.magic(
              Js.Dict.fromList(
                [(":PK", "STORY#" ++ storyID), (":SK", "LAYER#" ++ layerID)]
                ->List.concat(expressionAttributeValuesArray->List.fromArray),
              ),
            ),
          (),
        );
      },
    )
  ->Request.promise;
};

For the shake of transparency, I also published this in the Ocaml forum, because rescript one was down yesterday, I had the post already written and I also usually get some interesting answers there