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. npm imports are untrusted by default — calls are auto-wrapped in Result<T, Error>.
A package that is not installed is an error
Section titled “A package that is not installed is an error”Floe looks for the package in node_modules. Two different situations follow, and they get two different answers:
| The package | Floe reports | floe check |
|---|---|---|
is not in node_modules | E013, an error, and the fix to run | fails |
| is installed but ships no declarations | W004, a warning on each call | passes |
| is installed with declarations | nothing | passes |
The editor and the command line give the same answer, so a red line in your editor also fails continuous integration.
E013 names the command that fixes it:
error[E013]: cannot find module `"date-fns"` help: install the package: `npm install date-fns`. If it ships no type declarations, also add `npm install --save-dev @types/date-fns`W004 is different. The package is there, so the code runs, but Floe cannot type the symbol and cannot check the arguments you pass to it. Install the matching @types package to clear it.
What Floe never reports as a missing package
Section titled “What Floe never reports as a missing package”E013 names a package to install, so Floe stays quiet whenever the specifier is not a package or something other than node_modules resolves it:
| Specifier | Why | What Floe does |
|---|---|---|
node:fs, fs, fs/promises | a Node builtin. Node supplies the module, so it always resolves | warns W004 when @types/node is missing, and names it in the help. Never an error, so a Bun or Deno project still builds |
#lib/helper | package.json imports resolves it | nothing |
@app/lib/helper | a tsconfig paths alias | nothing. Floe reads the tsconfig next to your source file as well as the one at the project root |
src/lib/helper | a tsconfig baseUrl path | nothing |
| anything, under Yarn Plug’n’Play | there is no node_modules to read | nothing |
A project that has simply not been installed yet still reports E013 on every npm import, because the package really is absent and npm install really is the fix.
Untrusted imports (default)
Section titled “Untrusted imports (default)”All npm imports are untrusted by default. The compiler auto-wraps calls in Result<T, Error>:
import { parseYaml } from "yaml-lib"
// parseYaml is auto-wrapped — returns Result<T, Error>let result = parseYaml(input)match result { Ok(data) -> process(data), Err(e) -> Console.error(e),}Use ? to unwrap the result concisely:
let data = parseYaml(input)? // unwraps or returns Err earlytrusted imports
Section titled “trusted imports”For npm functions known to be safe (like React hooks, utility libraries), mark them with trusted so they can be called directly without Result wrapping:
import trusted { useState, useEffect } from "react"
let (count, setCount) = useState(0) // direct call, no wrappingYou can mark individual functions as trusted from a module:
import { trusted capitalize, fetchData } from "some-lib"
capitalize("hello") // direct call, no wrapping (trusted)let data = fetchData() // Result<T, Error> — auto-wrapped (untrusted)Bridging TypeScript types
Section titled “Bridging TypeScript types”Every Floe type declaration has the shape type Name = RHS. For interop with TypeScript libraries, two utility types bridge the structural operators TS uses:
OneOf<A, B, ...>compiles toA | B | ...Intersect<A, B, ...>compiles toA & B & ...
Plain aliases and TS utility types (Partial, Pick, ReturnType, …) work directly on the RHS.
String-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";In Floe, wrap them in OneOf<>:
typealias HttpMethod = OneOf<"GET", "POST", "PUT", "DELETE">
let 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 a TypeScript string union (no tags, no wrapping).
Writing a bare string-literal union (type M = "a" | "b") is a compile error (E201). Top-level | always declares nominal variants in Floe; OneOf<> is how you ask for the structural form.
Type aliases
Section titled “Type aliases”Alias TypeScript types with plain =:
import { ComponentProps } from "react"
typealias DivProps = ComponentProps<"div">typealias PartialUser = Partial<User>typealias UserKeys = Pick<User, OneOf<"name", "email">>Intersections
Section titled “Intersections”Combine TypeScript types with Intersect<>:
import { tv, VariantProps } from "tailwind-variants"
let cardVariants = tv({ base: "rounded-xl", variants: { size: { sm: "p-2" } } })typealias CardProps = Intersect<VariantProps<typeof cardVariants>, { className: string }>For Floe-native record composition, prefer ...Spread in a { } record body:
type CardProps = { ...VariantProps<typeof cardVariants>, className: string,}Function-type aliases
Section titled “Function-type aliases”Use -> for function types. Parameter labels are optional documentation:
import { Request, Response } from "express"
typealias Handler = (req: Request, res: Response) -> Promise<()>Nullable and optional type conversion
Section titled “Nullable and optional type conversion”Floe has no null or undefined. When importing from TypeScript, the compiler converts nullable and optional types automatically:
| TypeScript type | Floe type |
|---|---|
T | null | Option<T> |
T | undefined | Option<T> |
T | null | undefined | Option<T> |
x?: T (function param) | x: Option<T> = None |
x?: T | null | Settable<T> = Unchanged |
any | unknown |
Optional parameters (?) become Option<T> with a default of None, so you can omit them when calling:
import { 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:
import { useState, useEffect, useCallback } from "react"
export let Counter() -> JSX.Element = { let (count, setCount) = useState(0)
useEffect(() -> { Console.log("count changed:", count) }, [count])
<button onClick={() -> 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 { Button, Dialog } from "@radix-ui/react"
export let MyPage() -> JSX.Element = { let (open, setOpen) = useState(false)
<div> <Button onClick={() -> setOpen(true)}>Open</Button> <Dialog open={open} onOpenChange={setOpen}> <p>Dialog content</p> </Dialog> </div>}Globals (browser and runtime APIs)
Section titled “Globals (browser and runtime APIs)”Browser globals like window, document, navigator, and fetch are available automatically — no imports needed. Floe reads your tsconfig.json to determine which globals exist:
// Browser project (lib includes "DOM")let url = window.location.hrefnavigator.clipboard.writeText("hello") |> awaitlet width = window.innerWidthFor non-browser runtimes, configure compilerOptions.lib and compilerOptions.types in your tsconfig.json:
// Node.js{ "compilerOptions": { "lib": ["ES2020"], "types": ["node"] } }// Now process, Buffer, etc. are availablelet env = process.envSee Configuration for details.
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.