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 Playground

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 fn Dashboard() -> JSX.Element {
  const [status, setStatus] = useState<Status>(Loading)

  status |> match {
    Loading -> <Spinner />,
    Failed(msg) -> <Alert message={msg} />,
    Ready(users) -> {
      const active = users
        |> filter(.active)
        |> sort_by(.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} />;
  }

  const 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
const activeNames = users
  |> filter(.isActive)
  |> sort_by(.lastLogin)
  |> map(.displayName)

// Placeholder _ controls argument position
const discounted = items
  |> map(.price)
  |> filter(less_than(_, 100))
  |> reduce(add, 0, _)

// Tap for side effects without breaking the chain
const result = data
  |> validate
  |> tap(Console.log)
  |> transform
  |> save

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
}

fn 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
fn getUser(id: string) -> Result<User, ApiError> {
  const response = fetch("/api/users/{id}")?
  const user = response.json()?

  Ok(user)
}

// The caller sees exactly what can go wrong
match getUser("123") {
  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