Skip to content

TypeScript Interop

Floe compiles to TypeScript, so you can use any existing TypeScript or React library directly. No bindings, no wrappers, no code generation.

Import from npm packages the same way you would in TypeScript:

import { useState, useEffect } from "react"
import { z } from "zod"
import { clsx } from "clsx"

The compiler reads .d.ts type definitions to understand the types of imported values.

By default, Floe treats npm imports as potentially throwing. The compiler requires you to wrap calls in try, which returns a Result<T, Error>:

import { parseYaml } from "yaml-lib"
// parseYaml might throw, so you must use try
const result = try parseYaml(input)
match result {
Ok(data) -> process(data),
Err(e) -> Console.error(e),
}

For libraries you know won’t throw, mark the import as trusted to skip the try requirement:

import trusted { useState, useEffect } from "react"
import trusted { clsx } from "clsx"
// No try needed - these are trusted
const [count, setCount] = useState(0)
const classes = clsx("btn", active)

You can also trust individual functions from a module:

import { trusted capitalize, fetchData } from "some-lib"
capitalize("hello") // trusted, no try needed
const data = try fetchData() // not trusted, try required

Many TypeScript libraries use string literal unions for configuration and options:

// React
type HTMLInputTypeAttribute = "text" | "password" | "email" | "number";
// API clients
type Method = "GET" | "POST" | "PUT" | "DELETE";

Floe supports these natively:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
fn describe(method: HttpMethod) -> string {
match method {
"GET" -> "fetching",
"POST" -> "creating",
"PUT" -> "updating",
"DELETE" -> "removing",
}
}

The match is exhaustive — if you miss a variant, the compiler tells you. The type compiles directly to the same TypeScript string union (no tags, no wrapping).

Floe has no null or undefined. When importing from TypeScript, the compiler converts nullable types automatically:

TypeScript typeFloe type
T | nullOption<T>
T | undefinedOption<T>
T | null | undefinedOption<T>
anyunknown
import trusted { getElementById } from "some-dom-lib"
// .d.ts says: getElementById(id: string): Element | null
// Floe sees: getElementById(id: string) -> Option<Element>
match getElementById("app") {
Some(el) -> render(el),
None -> Console.error("element not found"),
}

React hooks work directly. Use trusted since hooks don’t throw:

import trusted { useState, useEffect, useCallback } from "react"
export fn Counter() -> JSX.Element {
const [count, setCount] = useState(0)
useEffect(fn() {
Console.log("count changed:", count)
}, [count])
<button onClick={fn() setCount(count + 1)}>
{`Count: ${count}`}
</button>
}

Third-party React components work as regular JSX:

import trusted { Button, Dialog } from "@radix-ui/react"
export fn MyPage() -> JSX.Element {
const [open, setOpen] = useState(false)
<div>
<Button onClick={fn() setOpen(true)}>Open</Button>
<Dialog open={open} onOpenChange={setOpen}>
<p>Dialog content</p>
</Dialog>
</div>
}

Floe’s compiled output is standard TypeScript. Your build tool (Vite, Next.js, etc.) processes it like any other .ts file. There is no Floe-specific runtime or framework to install.