TypeScript Interop
Floe compiles to TypeScript, so you can use any existing TypeScript or React library directly. No bindings, no wrappers, no code generation.
Importing npm packages
Section titled “Importing npm packages”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.
trusted imports
Section titled “trusted imports”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 tryconst 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 trustedconst [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 neededconst data = try fetchData() // not trusted, try requiredString literal unions
Section titled “String literal unions”Many TypeScript libraries use string literal unions for configuration and options:
// Reacttype HTMLInputTypeAttribute = "text" | "password" | "email" | "number";
// API clientstype 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).
Nullable type conversion
Section titled “Nullable type conversion”Floe has no null or undefined. When importing from TypeScript, the compiler converts nullable types automatically:
| TypeScript type | Floe type |
|---|---|
T | null | Option<T> |
T | undefined | Option<T> |
T | null | undefined | Option<T> |
any | unknown |
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"),}Using React hooks
Section titled “Using React hooks”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>}Using React component libraries
Section titled “Using React component libraries”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>}Output
Section titled “Output”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.