Experimental — pre-1.0, APIs and syntax may change

A functional language for the TypeScript ecosystem

Import any TypeScript library into Floe. Import Floe from TypeScript. Types, functions, and React components work both ways.

Get Started Language Tour

Familiar syntax, stronger guarantees

If you know TypeScript, you can read Floe. Union types, pattern matching, and pipes replace the boilerplate you already write — with exhaustive checking built in.

app.fl
import trusted { useState } from "react"

type User = {
  name: string,
  role: string,
  active: boolean,
}

type Status =
  | Loading
  | Failed(string)
  | Ready(Array<User>)

export let Dashboard() -> JSX.Element = {
  let (status, setStatus) = useState<Status>(Loading)

  status |> match {
    Loading -> <Spinner />,
    Failed(msg) -> <Alert message={msg} />,
    Ready(users) -> {
      let active = users
        |> filter(.active)
        |> sortBy(.name)

      <div>
        <h2>{active |> length} active</h2>
        {active |> map((u) ->
          <Card key={u.name} title={u.name} badge={u.role} />
        )}
      </div>
    },
  }
}
app.tsx
import { useState } from "react";

type User = {
  name: string;
  role: string;
  active: boolean;
};

type Status =
  | { __tag: "Loading" }
  | { __tag: "Failed"; message: string }
  | { __tag: "Ready"; users: User[] };

export function Dashboard(): JSX.Element {
  const [status, setStatus] = useState<Status>(
    { __tag: "Loading" }
  );

  if (status.__tag === "Loading") {
    return <Spinner />;
  }

  if (status.__tag === "Failed") {
    return <Alert message={status.message} />;
  }

  let active = status.users
    .filter((u) => u.active)
    .sort((a, b) => a.name.localeCompare(b.name));

  return (
    <div>
      <h2>{active.length} active</h2>
      {active.map((u) => (
        <Card key={u.name} title={u.name} badge={u.role} />
      ))}
    </div>
  );
}

Pipes

Chain transformations in reading order with |>. Dot shorthands pull fields. Placeholders slot arguments where you want them.

pipeline.fl
// Dot shorthand — .field becomes an accessor function
let activeNames = users
  |> filter(.isActive)
  |> sortBy(.lastLogin)
  |> map(.displayName)

// Placeholder _ for non-first argument position
5 |> add(3, _)            // add(3, 5)
42 |> wrap("[", _, "]")   // wrap("[", 42, "]")

// Tap for side effects without breaking the chain
users
  |> filter(.active)
  |> tap(Console.log)
  |> map(.name)

Exhaustive pattern matching

Add a variant to a union type. The compiler flags every match that doesn't handle it yet.

route.fl
type Route =
  | Home
  | Profile(string)
  | Settings
  | NotFound

let render(route: Route) -> JSX.Element = {
  match route {
    Home -> <HomePage />,
    Profile(id) -> <ProfilePage id={id} />,
    Settings -> <SettingsPage />,
    NotFound -> <NotFoundPage />,
  }
}

Result and Option types

Floe has no null, undefined, or exceptions. Functions return Result or Option, and ? propagates errors up.

user.fl
let getUser(id: string) -> Promise<Result<User, ApiError>> = {
  let response = fetch("/api/users/{id}") |> await?
  let user = response.json() |> await?

  Ok(user)
}

// The caller sees exactly what can go wrong
match getUser("123") |> await {
  Ok(user) -> renderProfile(user),
  Err(NotFound) -> <p>User not found</p>,
  Err(Unauthorized) -> redirect("/login"),
}

Works inside existing projects

Add the Vite plugin and write .fl files alongside .ts. Import in either direction.

vite.config.ts
import floe from "@floeorg/vite-plugin"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [floe()],
})
then import .fl from TypeScript
main.tsx
import { App } from "./app.fl"

ReactDOM.createRoot(document.getElementById("root")!).render(<App />)

Get started

$ cargo install floe
Installation Guide Language Tour