A new programming language faces a paradox. It exists because existing languages have problems worth solving. But if it departs too radically from what developers already know, adoption stalls before anyone can experience the improvements. The history of programming languages is littered with technically superior designs that failed because they demanded too much unlearning on day one.
FLIN was born from a specific conviction: a 12-year-old in Cotonou should be able to write a full-stack web application in a single file, with no imports, no build tools, and no configuration. That vision required a new language. But we also recognized that billions of developers already think in JavaScript and TypeScript. Their muscle memory types function, not fn. Their instinct writes null, not none. Their fingers produce string, not text.
The question was never whether to accommodate those habits. It was how to accommodate them without compromising the language we were building.
The Philosophy: A Bridge, Not a Surrender
FLIN is not JavaScript. It was designed in Abidjan, named from the Fongbe phrase "E flin nu" (It remembers things), and built with a thirty-year horizon. It has its own type system, its own syntax, and its own philosophy about how code should be written.
But pragmatism matters. When a developer who has written JavaScript for ten years sits down to try FLIN, their first instinct will be to write what they know. If FLIN responds to that instinct with a cryptic error, we have lost that developer. If FLIN silently accepts it and does the right thing, we have gained a convert who will gradually learn the canonical FLIN way.
This is the principle we call "strategic aliasing." FLIN accepts JavaScript and TypeScript constructs as aliases -- a bridge that developers cross once, then leave behind. The formatter always outputs canonical FLIN syntax. The documentation teaches the FLIN way. But the compiler never punishes a developer for writing what they know.
Type Aliases
The most frequently typed constructs in any language are type names. A TypeScript developer writes string dozens of times per day. Telling them that FLIN uses text is fine in documentation. Giving them a compiler error the first time they type string is hostile.
FLIN's canonical types have their own names, chosen for clarity and universality. But the compiler accepts TypeScript equivalents at parse time and resolves them immediately:
// FLIN canonical syntax
entity User {
name: text
active: bool
score: float
}// Also valid -- TypeScript-friendly aliases entity User { name: string active: boolean score: number }
// Mixed usage is perfectly fine entity Product { title: string // TS alias price: float // FLIN canonical inStock: boolean // TS alias description: text // FLIN canonical } ```
The mapping is straightforward. string resolves to Type::Text. boolean resolves to Type::Bool. number and decimal both resolve to Type::Float. These aliases are soft keywords -- they can still be used as variable names if needed. The resolution happens at compile time with zero runtime overhead. And the formatter always outputs the canonical FLIN names, so code gradually normalizes as the team adopts the language.
We debated this decision extensively. One argument was that accepting string would confuse new developers about which name was "real." The counterargument was that rejecting string would confuse experienced developers about whether FLIN understood their intent. We chose the path of least friction. A developer who types string and sees the formatter change it to text learns the canonical name naturally, without frustration.
Null Compatibility
JavaScript has both null and undefined, a distinction that has confused developers for twenty-five years. TypeScript narrows the confusion but does not eliminate it. FLIN takes a clean position: there is exactly one "absence of value," and its canonical name is none.
But we accept null everywhere:
// FLIN canonical
x = none
if user != none { ... }// Also valid -- JS-friendly x = null if user != null { ... }
// In functions function findUser(id: int) { result = User.find(id) if result == null { return null } return result } ```
What FLIN does not accept is undefined. There is no concept of a variable that exists but has no value. In FLIN, a variable either has a value or it is none. This eliminates an entire class of bugs that JavaScript developers encounter regularly -- the difference between "this property is null" and "this property does not exist."
The triple-equals operator === is also absent. FLIN has one equality operator, ==, which handles type comparison sensibly. There is no need for a "strict" variant because FLIN's type system prevents the coercion ambiguities that === was invented to work around.
Function Declaration
FLIN's canonical function keyword is fn -- short, fast to type, and consistent with Rust, Go, and other modern languages. But JavaScript developers have typed function millions of times. Their fingers produce it automatically.
// FLIN canonical
fn greet(name: text) {
return "Hello, " + name
}// Also valid -- JS-friendly function greet(name: string) { return "Hello, " + name }
// Works with all modifiers async function fetchData() { ... } server function secretLogic() { ... } pub function publicHelper() { ... } ```
The function keyword is not a soft keyword -- it cannot be used as a variable name, just as fn cannot. Both map to the same AST node: Stmt::FnDecl. The compiler does not distinguish between them after parsing.
Variable Declaration
FLIN's canonical variable syntax is bare assignment: x = 5. No keyword needed. The variable is mutable by default, its type is inferred. This is the simplest possible syntax for the most common operation in programming.
But JavaScript developers expect let and const:
// All three are valid
name = "Alice" // FLIN canonical: mutable, inferred
let count = 0 // JS-friendly: mutable
const MAX_RETRIES = 3 // JS-friendly: immutableWhat FLIN does not accept is var. JavaScript's var keyword carries baggage -- function scoping, hoisting, the ability to redeclare variables. FLIN has none of these behaviors. Accepting var would either create false expectations about its semantics or require FLIN to implement JavaScript's scoping rules, which would contradict the language's design.
When a developer types var, FLIN gives a clear error: "FLIN uses bare assignment (x = 5) or let/const. var is not supported because FLIN has no hoisting."
Operators: Almost Entirely Compatible
Most JavaScript operators work identically in FLIN. Arithmetic, comparison, logical, bitwise, compound assignment, increment/decrement, exponentiation, spread/rest, optional chaining, nullish coalescing, and the ternary operator all behave exactly as a JavaScript developer expects:
// All of these work exactly like JavaScript
x = 5 + 3 * 2 // Arithmetic
valid = age >= 18 && active == true // Comparison + logical
name = user?.profile?.name ?? "Guest" // Optional chaining + nullish coalescing
items = [...oldItems, newItem] // Spread
result = condition ? "yes" : "no" // TernaryFLIN adds its own operators that have no JavaScript equivalent: the pipeline operator |>, ranges .. and ..=, the is keyword for runtime type checking, and the match arm arrow ->. These are additive -- they give developers new tools without breaking existing expectations.
What FLIN Intentionally Does Not Accept
Strategic compatibility does not mean universal compatibility. Some JavaScript constructs are intentionally absent because they conflict with FLIN's design philosophy.
Maps use square brackets, not curly braces. In FLIN, curly braces are reserved for code blocks. This avoids the ambiguity that JavaScript has between object literals and block statements:
// JavaScript habit -- gives a helpful error
data = { name: "Alice" }
// Error: FLIN uses ["key": value] for maps, not { key: value }.
// Example: data = ["name": "Alice", "age": 30]// Correct FLIN syntax data = ["name": "Alice", "age": 30] ```
Classes are replaced by entities and structs. JavaScript's class keyword bundles data and behavior together in ways that create complexity. FLIN separates the concerns: entities hold data (with automatic persistence, soft delete, and versioning), while structs hold logic:
// JavaScript class
class User {
constructor(name, email) { ... }
greet() { ... }
}// FLIN approach: data and behavior are separate entity User { name: text email: text @email }
struct UserService { fn greet(self, user: User) { return "Hello, " + user.name } } ```
Imports are absent entirely. FLIN uses auto-discovery: components, entities, and functions defined anywhere in the project are available everywhere. There is no need for import { X } from "./y" because the compiler already knows where everything is.
console.log() is accepted but rewritten. When a developer types console.log("hello"), the compiler silently rewrites it to log("hello"). This rewrite happens at compile time with zero overhead. The same applies to console.warn, console.error, console.info, and console.debug.
Error Messages as Migration Guides
When a JavaScript developer types something that FLIN does not accept, the error message serves as a migration guide rather than a dead end. We invested significant effort in Session 290 to make these messages genuinely helpful:
// Typing `name: String` (PascalCase)
Error: Use `text` or `string` for text types in FLIN// Typing items: array
Error: FLIN uses [type] for lists. Example: items: [text]
// Typing result: void
Error: FLIN functions without return value don't need a return type
// Typing n: double
Error: FLIN uses int for integers and float for decimals
```
Each error message tells the developer exactly what to type instead. There is no need to consult documentation. The compiler is the documentation.
The Side-by-Side Proof
The strongest argument for FLIN's approach is not any individual feature but the cumulative effect. Here is a complete Todo application in TypeScript (Express + Prisma + React) compared with FLIN:
// The ENTIRE Todo app in FLIN -- one file, 30 lines
entity Todo {
title: text
done: bool = false
}title = ""
fn add() { todo = Todo { title: title } save todo title = "" }
fn toggle(t: Todo) { t.done = !t.done save t }
My Todos
{for todo in Todo.all}
The TypeScript equivalent requires a Prisma schema file, an API route file with PrismaClient imports and async handlers, a React component file with useState, useEffect, and fetch calls -- at minimum three files and sixty-plus lines, plus a package.json, a build configuration, and a database connection string.
FLIN achieves the same result in one file with zero imports, zero configuration, and zero build step. The TypeScript developer reading this code can understand it immediately because the syntax is familiar. The entity keyword looks like a model. The save keyword is self-explanatory. The template syntax resembles JSX. The bind attribute works like React's controlled inputs but without the boilerplate.
The Thirty-Year Vision
FLIN accepts JavaScript and TypeScript aliases as a courtesy to existing developers. But the aliases are not the future. FLIN's canonical syntax -- text not string, none not null, fn not function, len(x) not x.length -- is designed for developers who have not yet learned to code.
The next generation will grow up with FLIN. They should not carry the weight of thirty years of JavaScript's historical accidents. That is why the formatter always outputs canonical FLIN, why the documentation teaches text and fn, and why the language has its own identity beneath the compatibility layer.
The bridge exists so that JavaScript developers can cross it. But FLIN is the destination, not the bridge.
---
This is Part 191 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.
Series Navigation: - [190] Previous article - [191] JavaScript and TypeScript Compatibility (you are here) - [192] Entity and Enum Patterns