   The `options` parameter is a keyword list of additional options controlling
   the behavior of this function. The available options are:
 
-  - `type`: Either `:template` or `:include`
+  - `type`: Either `:template` or `:include`, defaults to `:template`.
   - `includes`: A map where the key of each item is the name of the includable
     template, and the value associated with the key is a `Serum.Template`
     struct, which is an already compiled Serum template.
   """
-  @spec compile_files([Serum.File.t()], Template.template_type()) :: Result.t(map())
-  def compile_files(files, type) when is_atom(type) do
+  @spec compile_files([Serum.File.t()], options()) :: Result.t(map())
+  def compile_files(files, options) do
+    options = Keyword.merge(@default_options, options)
+
     result =
       files
-      |> Task.async_stream(&compile_file(&1, type))
+      |> Task.async_stream(&compile_file(&1, options))
       |> Enum.map(&elem(&1, 1))
       |> Result.aggregate_values(:template_loader)
 

      {:ct_error, e.description, e.line}
  end

  @spec expand_includes(Macro.t()) :: Macro.t()
  defp expand_includes(ast)

  defp expand_includes({:include, _, [arg]}) do
    case Template.get(arg, :include) do
      nil ->
        warn("There is no includable template named `#{arg}`.")
        nil

  @doc """
  Compiles the given EEx string.
  """
  @spec compile_string(binary(), options()) ::
          {:ok, Macro.t()}
          | {:ct_error, binary(), integer()}
  def compile_string(string, options) do
    compiled = EEx.compile_string(string)
    includes = options[:includes] || []

    ast =
      case options[:type] do
        :include -> compiled
        _ -> Macro.postwalk(compiled, &expand_includes(&1, includes))
      end

    {:ok, ast}
