Import any TypeScript library into Floe. Import Floe from TypeScript. Types, functions, and React components work both ways.
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.
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>
},
}
} 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>
);
}
Chain transformations in reading order with |>.
Dot shorthands pull fields. Placeholders slot arguments where you want them.
// 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)
Add a variant to a union type. The compiler flags every match
that doesn't handle it yet.
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 />,
}
}
Floe has no null, undefined, or exceptions.
Functions return Result or Option, and ? propagates errors up.
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"),
}
Add the Vite plugin and write .fl files alongside .ts.
Import in either direction.
import floe from "@floeorg/vite-plugin"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [floe()],
}) import { App } from "./app.fl"
ReactDOM.createRoot(document.getElementById("root")!).render(<App />)