function renderProperties (writer, addedProps, properties = {}, additionalProperties, required = [], fieldDefinitions = {}) {
  // Since Array.prototype.sort is guaranteed to be stable, we can sort by name first, then apply special sorting rules
  const keys = Object.keys(properties)
    .sort()
    .sort((a, b) => {
      // Sort PKs first
      if (fieldDefinitions[a]?.primaryKey === fieldDefinitions[b]?.primaryKey) {
        return 0
      }
      return fieldDefinitions[a]?.primaryKey ? -1 : 1
    })
  for (const name of keys) {
    const localProperty = properties[name]
    const { type, nullable, items } = localProperty
    addedProps.add(name)
    if (required.indexOf(name) !== -1) {
      writer.write(property(null, name))
    } else {
      writer.write(property(null, name))
      writer.write('?')
    }

    let types
    if (Array.isArray(type)) {
      types = type
    } else {
      types = [type]
    }

    if (nullable && types.indexOf('null') === -1) {
      types.push('null')
    }

    let first = true
    writer.write(': ')
    for (const type of types) {
      if (!first) {
        writer.write(' | ')
      }
      first = false
      if (type === 'null') {
        writer.write('null')
      } else if (type === 'array') {
        switch (items.type) {
          case 'object':
            writer.inlineBlock(() => {
              const current = items
              renderProperties(writer, addedProps, current.properties, current.additionalProperties, current.required)
            })
            writer.write('[]')
            break
            // TODO support arrays in arrays
          default:
            writer.write(`${JSONSchemaToTsType(items.type)}[]`)
        }
      } else if (type === 'object') {
        writer.inlineBlock(() => {
          const current = properties[name]
          renderProperties(writer, addedProps, current.properties, current.additionalProperties, current.required)
        })
      } else if (type === 'string' && localProperty.enum) {
        writer.write(localProperty.enum.sort().map((v) => `"${v}"`).join(' | '))
      } else {
        writer.write(JSONSchemaToTsType(type))
      }
    }

    writer.write(';')
    writer.newLine()
  }

  if (additionalProperties) {
    writer.write('[name: string]: any;')
  }
}
').newLine()
  writer.write(` * ${title}`).newLine()
  writer.write(` * ${description}`).newLine()
  writer.write('