docs: private static JsType forSoyType(SoyType soyType, boolean isIncrementalDom, boolean isStrict) {
      switch (soyType.getKind()) {
        case NULL:
          return NULL_OR_UNDEFINED_TYPE;
  
        case ANY:
          return ANY_TYPE;
  
        case UNKNOWN:
          return UNKNOWN_TYPE;
  
        case BOOL:
          return isStrict ? BOOLEAN_TYPE_STRICT : BOOLEAN_TYPE;
  
        case PROTO_ENUM:
          SoyProtoEnumType enumType = (SoyProtoEnumType) soyType;
          String enumTypeName = enumType.getNameForBackend(SoyBackendKind.JS_SRC);
          JsType.Builder enumBuilder =
              builder()
                  .addType(enumTypeName)
                  .addRequire(GoogRequire.create(enumTypeName))
                  .setPredicate(GOOG_IS_NUMBER);
          if (!isStrict) {
            // TODO(lukes): stop allowing number?, just allow the enum
            enumBuilder.addType("number");
          }
          return enumBuilder.build();
  
        case FLOAT:
        case INT:
          return NUMBER_TYPE;
  
        case STRING:
          return STRING_OR_UNSANITIZED_TEXT;
  
        case ATTRIBUTES:
          if (isIncrementalDom) {
            // idom has a different strategy for handling these
            return IDOM_ATTRIBUTES;
          }
          // fall through
        case HTML:
          if (isIncrementalDom) {
            // idom has a different strategy for handling these
            return IDOM_HTML;
          }
          // fall-through
        case CSS:
        case JS:
        case URI:
        case TRUSTED_RESOURCE_URI:
          return isStrict
              ? SANITIZED_TYPES_STRICT.get(((SanitizedType) soyType).getContentKind())
              : SANITIZED_TYPES.get(((SanitizedType) soyType).getContentKind());
  
        case LIST:
          ListType listType = (ListType) soyType;
          if (listType.getElementType().getKind() == SoyType.Kind.ANY) {
            return RAW_ARRAY_TYPE;
          }
          JsType element = forSoyType(listType.getElementType(), isIncrementalDom, isStrict);
          return builder()
              .addType("!Array<" + element.typeExpr() + ">")
              .addRequires(element.getGoogRequires())
              .setPredicate(GOOG_IS_ARRAY)
              .build();
  
        case LEGACY_OBJECT_MAP:
          {
            LegacyObjectMapType mapType = (LegacyObjectMapType) soyType;
            if (mapType.getKeyType().getKind() == SoyType.Kind.ANY
                && mapType.getValueType().getKind() == SoyType.Kind.ANY) {
              return RAW_OBJECT_TYPE;
            }
            JsType keyTypeName = forSoyType(mapType.getKeyType(), isIncrementalDom, isStrict);
            JsType valueTypeName = forSoyType(mapType.getValueType(), isIncrementalDom, isStrict);
            return builder()
                .addType(
                    String.format("!Object<%s,%s>", keyTypeName.typeExpr(), valueTypeName.typeExpr()))
                .addRequires(keyTypeName.getGoogRequires())
                .addRequires(valueTypeName.getGoogRequires())
                .setPredicate(GOOG_IS_OBJECT)
                .build();
          }
        case MAP:
          {
            MapType mapType = (MapType) soyType;
            SoyType keyType = mapType.getKeyType();
            SoyType.Kind keyKind = keyType.getKind();
            Preconditions.checkState(MapType.isAllowedKeyType(keyType));
            // Soy key type of string should translate to a JS key type of string.
            // forSoyType(StringType.getInstance()) normally translates to
            // string|!goog.soy.data.UnsanitizedText, but ES6 Maps always use instance equality for
            // lookups. Using UnsanitizedText instances as keys in Soy maps would cause unexpected
            // behavior (usually a failed map lookup), so don't generate signatures that allow it.
            JsType keyTypeName =
                keyKind == SoyType.Kind.STRING
                    ? STRING_TYPE
                    : forSoyType(keyType, isIncrementalDom, isStrict);
            JsType valueTypeName = forSoyType(mapType.getValueType(), isIncrementalDom, isStrict);
            return builder()
                .addType(
                    String.format(
                        "!soy.map.Map<%s,%s>", keyTypeName.typeExpr(), valueTypeName.typeExpr()))
                .addRequires(keyTypeName.getGoogRequires())
                .addRequires(valueTypeName.getGoogRequires())
                .addRequire(GoogRequire.create("soy.map"))
                .setPredicate(SOY_MAP_IS_SOY_MAP)
                .build();
          }
        case PROTO:
          final SoyProtoType protoType = (SoyProtoType) soyType;
          final String protoTypeName = protoType.getNameForBackend(SoyBackendKind.JS_SRC);
          return builder()
              .addType(
                  isStrict
                      ? "!" + protoTypeName
                      // In theory this should be "!" + protoTypeName since we don't actually
                      // allow null, but it isn't clear that this is very useful for users.
                      : protoTypeName)
              .addRequire(GoogRequire.create(protoTypeName))
              .addCoercionStrategy(ValueCoercionStrategy.PROTO)
              .setPredicate(
                  (value, codeGenerator) ->
                      Optional.of(value.instanceOf(JsRuntime.protoConstructor(protoType))))
              .build();
  
        case RECORD:
          {
            RecordType recordType = (RecordType) soyType;
            if (recordType.getMembers().isEmpty()) {
              return RAW_OBJECT_TYPE;
            }
            Builder builder = builder();
            Map<String, String> members = new LinkedHashMap<>();
            for (Map.Entry<String, SoyType> member : recordType.getMembers().entrySet()) {
              JsType forSoyType = forSoyType(member.getValue(), isIncrementalDom, isStrict);
              builder.addRequires(forSoyType.getGoogRequires());
              members.put(
                  member.getKey(), forSoyType.typeExprForRecordMember(/* isOptional= */ false));
            }
            return builder
                // trailing comma is important to prevent parsing ambiguity for the unknown type
                .addType("{" + Joiner.on(", ").withKeyValueSeparator(": ").join(members) + ",}")
                .setPredicate(GOOG_IS_OBJECT)
                .build();
          }
  
        case UNION:
          {
            UnionType unionType = (UnionType) soyType;
            Builder builder = builder();
            final Set<JsType> types = new LinkedHashSet<>();
            final boolean isNullable = unionType.isNullable();
            // handle null first so that if other type tests dereference the param they won't fail
            if (isNullable) {
              builder.addTypes(NULL_OR_UNDEFINED_TYPE.typeExpressions);
              builder.addCoercionStrategy(ValueCoercionStrategy.NULL);
              types.add(NULL_OR_UNDEFINED_TYPE);
            }
            for (SoyType member : unionType.getMembers()) {
              if (member.getKind() == Kind.NULL) {
                continue; // handled above
              }
              JsType memberType = forSoyType(member, isIncrementalDom, isStrict);
              builder.addRequires(memberType.extraRequires);
              builder.addTypes(memberType.typeExpressions);
              builder.addCoercionStrategies(memberType.coercionStrategies);
              types.add(memberType);
            }
            return builder
                .setPredicate(
                    (value, codeGenerator) -> {
                      Expression result = null;
                      // TODO(lukes): this will cause reevaluations, resolve by conditionally
                      // bouncing into a a temporary variable or augmenting the codechunk api to do
                      // this automatically.
                      for (JsType memberType : types) {
                        Optional<Expression> typeAssertion =
                            memberType.getTypeAssertion(value, codeGenerator);
                        if (!typeAssertion.isPresent()) {
                          return Optional.absent();
                        }
                        if (result == null) {
                          result = typeAssertion.get();
                        } else {
                          result = result.or(typeAssertion.get(), codeGenerator);
                        }
                      }
                      return Optional.of(result);
                    })
                .build();
          }
        case VE:
          return VE_TYPE;
        case VE_DATA:
          return VE_DATA_TYPE;
        case ERROR:
          // continue
      }
      throw new AssertionError("unhandled soytype: " + soyType);
    }