docs: private String genParamsRecordType(TemplateNode node) {
      Set<String> paramNames = new HashSet<>();
  
      // Generate members for explicit params.
      Map<String, String> record = new LinkedHashMap<>();
      for (TemplateParam param : node.getParams()) {
        JsType jsType = getJsTypeForParamForDeclaration(param.type());
        record.put(
            param.name(), jsType.typeExprForRecordMember(/* isOptional= */ !param.isRequired()));
        for (GoogRequire require : jsType.getGoogRequires()) {
          // TODO(lukes): switch these to requireTypes
          jsCodeBuilder.addGoogRequire(require);
        }
        paramNames.add(param.name());
      }
  
      // Do the same for indirect params, if we can find them.
      // If there's a conflict between the explicitly-declared type, and the type
      // inferred from the indirect params, then the explicit type wins.
      // Also note that indirect param types may not be inferrable if the target
      // is not in the current compilation file set.
      IndirectParamsInfo ipi =
          new IndirectParamsCalculator(templateRegistry)
              .calculateIndirectParams(templateRegistry.getMetadata(node));
      // If there are any calls outside of the file set, then we can't know
      // the complete types of any indirect params. In such a case, we can simply
      // omit the indirect params from the function type signature, since record
      // types in JS allow additional undeclared fields to be present.
      if (!ipi.mayHaveIndirectParamsInExternalCalls && !ipi.mayHaveIndirectParamsInExternalDelCalls) {
        for (String indirectParamName : ipi.indirectParamTypes.keySet()) {
          if (paramNames.contains(indirectParamName)) {
            continue;
          }
          Collection<SoyType> paramTypes = ipi.indirectParamTypes.get(indirectParamName);
          SoyType combinedType = SoyTypes.computeLowestCommonType(typeRegistry, paramTypes);
          // Note that Union folds duplicate types and flattens unions, so if
          // the combinedType is already a union this will do the right thing.
          // TODO: detect cases where nullable is not needed (requires flow
          // analysis to determine if the template is always called.)
          SoyType indirectParamType =
              typeRegistry.getOrCreateUnionType(combinedType, NullType.getInstance());
          JsType jsType = getJsTypeForParamForDeclaration(indirectParamType);
          // NOTE: we do not add goog.requires for indirect types.  This is because it might introduce
          // strict deps errors.  This should be fine though since the transitive soy template that
          // actually has the param will add them.
          record.put(indirectParamName, jsType.typeExprForRecordMember(/* isOptional= */ true));
        }
      }
      StringBuilder sb = new StringBuilder();
      sb.append("{\n *  ");
      Joiner.on(",\n *  ").withKeyValueSeparator(": ").appendTo(sb, record);
      // trailing comma in record is important in case the last record member is the
      // unknown type
      sb.append(",\n * }");
      return sb.toString();
    }