% ts-type-system.tex — how the TypeScript checker reasons: structural typing, types as
% sets (lattice, unions/intersections), narrowing + control-flow analysis, discriminated
% unions, top/bottom types, literals/widening, satisfies, readonly, optionality, variance of
% function parameters. Source: own knowledge. Senior-interview level (Swift background).
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/typescript/ts-type-system.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=typescript kind=concept level=core platform=web new=no round=typescript-2026-09-24 topic=language
% @tags: structural-typing, narrowing, discriminated-unions, type-guards, unknown-vs-any, never, satisfies, excess-property-check, interface-vs-type, as-const, exhaustiveness, bivariance
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}

\lstdefinelanguage{TSSheet}{
  morekeywords={import,from,export,default,const,let,type,interface,extends,function,
    return,async,await,if,else,switch,case,new,true,false,null,undefined,typeof,keyof,
    as,satisfies,never,string,number,boolean,void,declare,namespace,readonly},
  morekeywords={unknown,any,is,asserts,throw,in,instanceof},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/},
  morestring=[b]", morestring=[b]', morestring=[b]`}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
  tn/.style={font=\ttfamily\scriptsize, inner sep=1.2pt},
}

% 0xProto ligates => == != inside \texttt; a zero kern splits the glyph run.
\newcommand\fa{=\kern0pt>}
\newcommand\eqq{=\kern0pt=}
\newcommand\neqq{!\kern0pt=\kern0pt=}

\begin{document}

\sheettitle{TypeScript type system — how the checker reasons}{typescript · memo}

\oneliner{A type is a \textbf{set of values}; assignability is \textbf{subset}, judged by
\textbf{shape} (structural), not by name. Unions must be \textbf{narrowed} along control flow
before use — and it is all \textbf{erased} at runtime. Soundness is traded for ergonomics
in known holes: \texttt{any}, \texttt{as}, method bivariance, readonly props.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \foreach \x in {5.9,10.9} \draw[sheetGrey!40] (\x,3.0) -- (\x,-0.6);
  % ── 1 lattice ──
  \node[pt] at (0,2.85) {\textcolor{sheetBlue}{1} the lattice (up = bigger set)};
  \node[tn, draw=sheetBlue, fill=sheetBlue!8] (unk) at (2.6,2.3) {unknown};
  \node[tn] (str) at (0.55,1.55) {string};
  \node[tn] (num) at (1.55,1.55) {number};
  \node[tn] (ox)  at (2.95,1.55) {\{x\}};
  \node[tn] (nul) at (4.05,1.55) {null};
  \node[tn] (und) at (4.95,1.55) {undefined};
  \node[tn] (la)  at (0.25,0.85) {'a'};
  \node[tn] (lb)  at (0.85,0.85) {'b'};
  \node[tn] (l1)  at (1.55,0.85) {42};
  \node[tn] (oxy) at (2.95,0.85) {\{x,y\}};
  \node[tn, draw=sheetRed, fill=sheetRed!8] (nev) at (2.6,0.05) {never};
  \foreach \n in {str,num,ox,nul,und} \draw[sheetGrey] (unk) -- (\n);
  \draw[sheetGrey] (str) -- (la); \draw[sheetGrey] (str) -- (lb); \draw[sheetGrey] (num) -- (l1);
  \draw[sheetGrey] (ox) -- (oxy);
  \foreach \n in {la,lb,l1,oxy,nul,und} \draw[sheetGrey!60] (\n) -- (nev);
  \node[tn, draw=sheetOrange, dashed, fill=sheetOrange!10] (any) at (5.6,2.3) {any};
  \draw[hot, <->, dashed] (any) -- (unk);
  \draw[hot, <->, dashed] (any.south) |- (nev.east);
  \node[lbl, text=sheetOrange, anchor=east] at (5.5,1.0) {\texttt{any}: both ways,};
  \node[lbl, text=sheetOrange, anchor=east] at (5.5,0.8) {off the lattice};
  \node[lbl, anchor=west, text=sheetGreen!50!black] at (0,-0.28) {\texttt{\{x,y\}} under \texttt{\{x\}}: more props = \emph{smaller} set};
  \node[lbl, anchor=west] at (0,-0.5) {\texttt{A|B} = join (up) · \texttt{A\&B} = meet (down) · \texttt{string\&number} = never};
  % ── 2 union vs intersection ──
  \node[pt] at (6.0,2.85) {\textcolor{sheetBlue}{2} values, not members};
  \fill[sheetBlue!12] (7.55,1.25) circle (1.0);
  \fill[sheetGreen!14] (8.85,1.25) circle (1.0);
  \begin{scope}\clip (7.55,1.25) circle (1.0); \fill[sheetOrange!30] (8.85,1.25) circle (1.0); \end{scope}
  \draw[sheetBlue] (7.55,1.25) circle (1.0); \draw[sheetGreen] (8.85,1.25) circle (1.0);
  \node[font=\scriptsize, text=sheetBlue] at (7.05,1.55) {A};
  \node[lbl] at (7.05,1.2) {has \texttt{name}};
  \node[font=\scriptsize, text=sheetGreen!55!black] at (9.35,1.55) {B};
  \node[lbl] at (9.35,1.2) {has \texttt{age}};
  \node[lbl, text=sheetOrange!80!black] at (8.2,1.3) {\textbf{A\&B}\\name\\+ age};
  \node[lbl, anchor=west] at (6.0,-0.02) {\texttt{A\&B}: fewer values, \textbf{more} members};
  \node[lbl, anchor=west] at (6.0,-0.27) {\texttt{A|B}: more values, only \textbf{common} members};
  \node[lbl, anchor=west, text=sheetRed] at (9.6,2.45) {\texttt{u.age} on};
  \node[lbl, anchor=west, text=sheetRed] at (9.6,2.25) {\texttt{A|B}: error};
  % ── 3 control-flow narrowing ──
  \node[pt] at (11.0,2.85) {\textcolor{sheetBlue}{3} control-flow narrowing};
  \node[sb, minimum width=36mm] (n0) at (13.5,2.3) {\texttt{x: string | number | null}};
  \node[sb, minimum width=36mm, draw=sheetOrange, fill=sheetOrange!8] (n1) at (13.5,1.55) {\texttt{if (x \eqq{} null) return;}};
  \node[sb] (n2) at (13.5,0.85) {\texttt{typeof x} is \texttt{'string'}?};
  \node[sb, draw=sheetGreen, fill=sheetGreen!10] (yes) at (12.1,0.05) {\texttt{x: string}};
  \node[sb, draw=sheetGreen, fill=sheetGreen!10] (no)  at (14.9,0.05) {\texttt{x: number}};
  \draw[flow] (n0) -- (n1);
  \draw[flow] (n1) -- node[lbl, right]{\texttt{string|number}} (n2);
  \draw[flow] (n2) -| node[lbl, left, pos=0.75]{yes} (yes);
  \draw[flow] (n2) -| node[lbl, right, pos=0.75]{no: the rest} (no);
  \node[lbl, text=sheetGrey] at (13.5,-0.35) {each branch sees the set minus what was ruled out};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item \textbf{Structural}: \texttt{\{x,y\}} fits wherever \texttt{\{x\}} is expected
        (Swift: \textbf{nominal}). \texttt{private}/\texttt{\#private} fields make a class
        effectively nominal.
  \item \textbf{Excess-property check} — the exception: a \emph{fresh} object literal
        assigned straight to a typed target may not carry unknown keys (typo catcher);
        via a variable it passes.
  \item \textbf{interface vs type}: interfaces \textbf{merge} (re-declare = augment globals,
        lib types); \texttt{extends} reports conflicts \emph{at the declaration} and is
        cached by name (cheaper than big \texttt{\&}). Only \texttt{type} does unions,
        tuples, mapped/conditional. \texttt{\&} turns a conflicting prop into \texttt{never}.
  \item \textbf{Narrowing}: \texttt{typeof} (\texttt{"object"} includes \texttt{null}!),
        \texttt{instanceof} (prototype chain), \texttt{"k" in x}, equality
        (\texttt{\eqq{} null} catches null \emph{and} undefined; tag compare), truthiness
        (also drops \texttt{0}, \texttt{""}, \texttt{NaN}). Assignments re-widen; a
        reassigned \texttt{let} loses narrowing in callbacks (5.4: kept after the last
        assignment).
  \item \textbf{Guards}: \texttt{(x): x is Fish} — the body is \emph{trusted, not checked}.
        \texttt{asserts x is T} / \texttt{asserts cond} narrows \emph{after} the call.
        5.5 infers predicates: \texttt{arr.filter(x \fa{} x \neqq{} undefined)}.
  \item \textbf{Top/bottom}: \texttt{unknown} takes anything, gives nothing until narrowed.
        \texttt{never} = empty set: assignable to all, nothing to it. \texttt{any} switches checking off \emph{and spreads}. \texttt{void} =
        ``result ignored'': a \texttt{() \fa{} void} slot accepts \texttt{() \fa{} number}.
  \item \textbf{Widening}: \texttt{const s = 'a'} is \texttt{'a'}; \texttt{let} and object
        props widen to \texttt{string}. \texttt{as const} = deep \texttt{readonly} + literals.
  \item \textbf{Optionality}: \texttt{a?: T} may be \emph{absent}; \texttt{a: T|undefined}
        must be \emph{present}. \texttt{exactOptionalPropertyTypes} forbids writing
        \texttt{a: undefined} into \texttt{a?: T}.
  \item \textbf{Functions}: fewer params is fine (\texttt{forEach(x \fa{} …)}); return is
        covariant; params contravariant under \texttt{strictFunctionTypes} — \emph{except
        method syntax} \texttt{m(x: T): void}, kept \textbf{bivariant} (so
        \texttt{Dog[]} fits \texttt{Animal[]}).
\end{itemize}

\section{Example — discriminated union, guards}
\noindent\begin{minipage}{\linewidth}
\begin{lstlisting}[language=TSSheet]
type Shape =                  // Swift: enum + associated values
  | { kind: 'circle'; r: number }
  | { kind: 'rect'; w: number; h: number };
function area(s: Shape): number {
  switch (s.kind) {           // the tag narrows the union
    case 'circle': return Math.PI * s.r ** 2;
    case 'rect':   return s.w * s.h;
    default: return s satisfies never; // new kind -> error
  }
}
const isStr = (x: unknown): x is string => typeof x === 'string';
function check(c: unknown, msg: string): asserts c {
  if (!c) throw new Error(msg);
}
\end{lstlisting}
\end{minipage}

\section{annotation · satisfies · as}
\begin{lstlisting}[language=TSSheet]
const r1: Record<string, string> = { home: '/' };
r1.hmoe;       // compiles: the annotation IS the type now
const r2 = { home: '/' } satisfies Record<string, string>;
r2.hmoe;       // error: inferred keys kept (TS 4.9)
const r3 = {} as Record<'home', string>;  // compiles, lies
interface P { x: number }
const p: P = { x: 1, y: 2 };    // error: excess property
const t = { x: 1, y: 2 }; const q: P = t;  // OK: structural
\end{lstlisting}

\section{Swift $\to$ TS}
{\footnotesize\begin{tabular}{@{}p{0.43\linewidth}@{\;}p{0.53\linewidth}@{}}
\toprule
Swift & TypeScript \\ \midrule
protocol conformance (nominal) & any matching shape (structural) \\
enum + associated values & union with a literal \texttt{kind} tag \\
compiler-exhaustive \texttt{switch} & \texttt{const x: never} in \texttt{default} \\
\texttt{Optional<Optional<T>>} nests & \texttt{T|undefined|undefined} collapses \\
\texttt{as!} traps at runtime & \texttt{as} never checks anything \\
\texttt{Any} & \texttt{unknown} (not \texttt{any}) \\
\bottomrule
\end{tabular}}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{\texttt{\{\}} means ``any non-nullish value'' (even \texttt{5}), not ``empty object'';
        \texttt{object} = non-primitive; empty = \texttt{Record<string, never>}.}
  \trap{\texttt{\{readonly x\}} \emph{is} assignable to \texttt{\{x\}} (hole); but
        \texttt{readonly T[]} is not assignable to \texttt{T[]}. \texttt{readonly} is
        shallow and compile-time only.}
  \trap{A wrong \texttt{x is T} guard is an unchecked lie — worse than \texttt{as}, it
        hides at every call site.}
  \trap{Callbacks declared as methods are bivariant: write \texttt{onX: (e: E) \fa{} void}
        (property) to get strict checks.}
  \trap{\texttt{if (count)} also drops \texttt{0}: compare with \texttt{undefined}.}
  \trap{\texttt{as} refuses only types that don't overlap at all;
        \texttt{x as unknown as T} forces anything — grep for it in review.}
\end{itemize}

\section{Remember}
\emph{Types are sets · assignable = subset · shape, not name · narrow before you touch.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item \texttt{type} or \texttt{interface}? — interface for object contracts; \texttt{type}
        for unions and computed types.
  \item \texttt{unknown} vs \texttt{any}? — both accept all; \texttt{unknown} forces a
        narrow before use, \texttt{any} silently disables checking downstream.
  \item \texttt{satisfies} vs \texttt{as}? — validates yet keeps the inferred type; \texttt{as}
        asserts, checked only for ``sufficient overlap''.
  \item Exhaustiveness? — assign the leftover to \texttt{never}.
  \item Why is an extra key rejected only sometimes? — the excess-property check fires
        on \emph{fresh} literals only.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} ts-generics-advanced ·
ts-config-and-practice · rn-testing-typescript (props, nav params, Codegen) ·
Swift enums \& protocols · \texttt{some}/\texttt{any}}

\end{document}
