TypeFighter
TypeFighter is a small, experimental language built around a modern, inference-first type system. The headline feature is structural records instead of nominal ones: records are compared by the fields they actually have, not by a declared name — so a function that needs { name: String } accepts any record with that field, no boilerplate declarations required. On top of that: row polymorphism, set-theoretic literal and union types, and classical polymorphic functions — all figured out by the type checker with (almost) no annotations. This site is generated from the test suite: each test is a minimal example of what the type system can — and can't yet — do.
What's in, what's not
A quick comparison against concepts you'll recognize from Rust, Haskell, Scala, TypeScript, Swift, or F#:
In today
- Structural records — matched by fields, not by name
- Row polymorphism — "has at least field X"
- Literal & union types — booleans are modelled as the union
true | false; each literal is a type on its own - Intersection types (partial)
- Polymorphic functions — fresh instantiation at every use
- Higher-order & partial application
- Arrays as a built-in type constructor
- Inference with (almost) no annotations
Not yet (or absent by design)
- Let-generalization — inner
letbindings don't auto-generalize the way classical Hindley–Milner does - Traits / type classes / protocols — no ad-hoc polymorphism (see below)
- Sum types / ADTs — no
Option/Result-style constructors; literal unions cover a subset - Pattern matching — no
match … withconstruct - Recursion — no
let recor fix-point combinator - Implicit conversions / subtyping —
Numberis never silently aString - Nominal types — everything is structural (by design, not a gap)
- Surface syntax — programs are built via the F# DSL, there is no parser from text
- Effect system — effects aren't tracked
A note on Traits
In languages like Rust (traits), Haskell (type classes), Swift (protocols), or Scala (given/implicits), you can write a function like show : a -> String that works on any type a which implements a Show constraint. TypeFighter has no such mechanism — there is no way to say "any type that supports toString" or "any Num". The closest thing TypeFighter offers is row polymorphism: a function fun r -> r.name works on any record that structurally has a name field. That gives you "works on anything with field X" — but not "works on anything supporting operation X".
How to read these pages
Every test is shown with three things:
- A short doc block giving the environment, source expression, and inferred type (or expected error).
- An optional note with extra context about what the test demonstrates.
- The raw F# test body — toggle it open to see the AST construction.
Tests marked Not yet implemented document features that are deliberately not supported today.
Categories (49 tests total)
Numbers, strings, and booleans are the primitive literal forms. Booleans are modeled set-theoretically as the union of the two literal values true and false — so every boole…
Lambda abstractions, application, partial application, and the shape of higher-order types. A lambda without usage constraints generalizes to a polymorphic type at its enclosing…
let introduces a name with two key properties: 1. Scope — the name is in scope only within the body. 2. Generalization — if the bound value has free type variables, those are…
Turning a MonoTyp with free type variables into a PolyTyp binds those variables as forall-quantified. Alpha-equivalent polytypes (same shape, different var numbers) are normaliz…
Arrays are homogeneous — all elements must unify to a single element type. Mixing incompatible element types is a static error.
This file covers record creation and flat property access on fully-known records. For row-polymorphic access through lambda parameters, see Rows.fs.
When a function accesses fields of a record it receives as an argument, the inferencer collects the required fields and reconstructs a record type that contains exactly those…
Polytypes provided through the environment are instantiated fresh at every use site — the same name can be used at different types. For AST-level let generalization, see Let.fs.
An intersection R1 & R2 behaves as a value that conforms to both record shapes. Field access is only allowed when both sides agree on the field name and its type.
Scenarios where one type could be coerced to another — for example where a String is expected but a Number is given. Currently unsupported: unification rejects the mismatch…