8. Conditional types and infer
Sometimes the type you want is a function of another type: "the element type of
this list", "the return type of that function", "the type of the port field
of this config". tynix borrows TypeScript's conditional types for this.
Checked extends Pattern ? WhenMatched : OtherwiseIf Checked matches Pattern, the type reduces to WhenMatched, otherwise to
Otherwise. Inside Pattern, infer x captures part of the matched type so
that WhenMatched can use it.
A small toolbox
type ElementOf t = t extends List (infer a) ? a : t;
type ReturnOf f = f extends (infer a -> infer r) ? r : dynamic;
type ArgOf f = f extends (infer a -> infer r) ? a : dynamic;
type FieldOf r = r extends { value :: infer v; } ? v : unknown;
type IsString t = t extends String ? true : false;
let
item :: ElementOf (List String);
item = "hello";
same :: ElementOf Int;
same = 3;
result :: ReturnOf (String -> Int);
result = 42;
arg :: ArgOf (String -> Int);
arg = "input";
field :: FieldOf { value :: Bool; label :: String; };
field = true;
yes :: IsString "nix";
yes = true;
in { inherit item same result arg field yes; }tynix check conditional.tynixEvery binding checks. Output keeps the alias spelling (item :: ElementOf (List String)) because that is what you wrote, but the checker compares values
against the reduced type:
| Annotation | Reduces to | Why |
|---|---|---|
ElementOf (List String) |
String |
List String matches List (infer a) with a = String |
ElementOf Int |
Int |
no match, so the else branch returns t |
ReturnOf (String -> Int) |
Int |
the function pattern binds a = String, r = Int |
ArgOf (String -> Int) |
String |
same match, other variable |
FieldOf { value :: Bool; label :: String; } |
Bool |
record patterns match by field; extra fields are fine |
IsString "nix" |
true |
no infer, so tynix falls back to subtyping: "nix" <: String |
Change result = 42; to a string and the reduced type shows its teeth:
15:12: [TC0013] type mismatch: "forty-two" vs ReturnOf (String -> Int)How matching works
tynix reduces a conditional type in two stages:
- Pattern match. If the pattern contains
infer, tynix matches the checked type against it structurally: functions against functions, records field by field, applications such asList aargument by argument. Eachinfer xbinds the part it lines up with. Using the sameinfer xtwice requires both occurrences to bind the same type. - Subtype test. If the pattern does not match structurally, tynix asks
whether
Checkedis a subtype ofPatternand picks the branch from the answer. This is howIsString "nix"works.
Two practical rules follow:
- Conditional types do not distribute over unions.
ElementOf (List Int | String)does not becomeInt | String. The union as a whole does not matchList (infer a), so the result is theelsebranch, the union itself. - Function patterns match the arrow exactly. A pattern written with
->does not match a linear%1 ->function type and vice versa. Write the pattern with the arrow you expect to receive.
Warning
Keep conditional aliases non-recursive. A conditional alias that refers to itself in a branch, such as
Unwrap t = t extends { value :: infer v; } ? Unwrap v : t, is cut off by tynix's reduction budget and does not reduce the way you would expect. Unroll the recursion to the depth you need instead.
Combining with real types
Conditional types are most useful on top of record types you already have. Here a config's port type is extracted once and reused, refinement and all:
type PortOf c = c extends { port :: infer p; } ? p : Int;
type Config = { port :: Range 1 65535 Int; host :: String; };
let
port :: PortOf Config;
port = 8080;
in portPortOf Config reduces to Range 1 65535 Int, a numeric refinement: integers
from 1 to 65535. port = 70000; is rejected with
6:10: [TC0013] type mismatch: 70000 vs PortOf Config. The
language reference covers Range,
Unit, and the Vec / Matrix / Tensor shape types.
Recap
A extends B ? C : Dpicks a branch at the type level.infer xinsideBcaptures a piece ofAfor use inC.- Without
infer, the test is plain subtyping. - No distribution over unions, exact arrow matching, and no recursion.