% ts-generics-advanced.tex — generics and type-level programming: constraints, inference,
% keyof/typeof/indexed access, conditional + infer, distribution, mapped types + remapping,
% template literal types, recursion, variance (in/out), utility types from scratch,
% const type parameters, NoInfer, depth limits. Source: own knowledge. Senior level.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/typescript/ts-generics-advanced.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=senior platform=web new=no round=typescript-2026-09-24 topic=language
% @tags: generics, conditional-types, infer, distributive-conditional, mapped-types, key-remapping, template-literal-types, variance, utility-types, keyof, const-type-parameters, noinfer
\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,infer,in,out,is},
  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>}

\begin{document}

\sheettitle{TypeScript generics \& type-level programming}{typescript · memo}

\oneliner{Type parameters are \textbf{solved by inference} at each use, bounded by
\texttt{extends}; \textbf{conditional}, \textbf{mapped} and \textbf{template-literal} types form a
small pure functional language the checker evaluates — erased at runtime, limited by
instantiation depth, and only as good as the inference it enables at call sites.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \foreach \x in {5.6,11.0} \draw[sheetGrey!40] (\x,3.0) -- (\x,-0.5);
  % ── 1 distribution ──
  \node[pt] at (0,2.85) {\textcolor{sheetBlue}{1} distributive conditional};
  \node[tn, anchor=west] at (0,2.35) {F<T> = T extends any ? T[] : never};
  \node[sb] (u) at (0.75,1.3) {\texttt{A|B|C}};
  \foreach \n/\y in {A/1.9,B/1.3,C/0.7} {
    \node[tn, draw=sheetGrey] (i\n) at (2.2,\y) {F<\n>};
    \draw[flow] (u.east) -- (i\n.west);
    \node[tn] (o\n) at (3.4,\y) {\n[]};
    \draw[flow] (i\n) -- (o\n);
  }
  \node[sb, draw=sheetGreen, fill=sheetGreen!10] (r) at (4.75,1.3) {\texttt{A[]|B[]|C[]}};
  \foreach \n in {A,B,C} \draw[flow] (o\n.east) -- (r.west);
  \node[lbl, text=sheetOrange, anchor=west] at (0,0.2) {\texttt{[T] extends [any]}: no fan-out $\to$ \texttt{(A|B|C)[]}};
  \node[lbl, anchor=west] at (0,-0.05) {only a \emph{naked} \texttt{T} distributes; \texttt{never} = empty union $\to$ \texttt{never}};
  \node[lbl, anchor=west] at (0,-0.3) {\texttt{Exclude}, \texttt{Extract}, \texttt{NonNullable}-style filters rely on it};
  % ── 2 variance ──
  \node[pt] at (5.7,2.85) {\textcolor{sheetBlue}{2} variance: does \texttt{<>} keep the arrow?};
  \node[tn] (d) at (6.4,2.2) {Dog}; \node[tn] (a) at (9.9,2.2) {Animal};
  \draw[flow] (d) -- node[lbl, above]{assignable to} (a);
  \node[tn] (pd) at (6.4,1.55) {Getter<Dog>}; \node[tn] (pa) at (9.9,1.55) {Getter<Animal>};
  \draw[->, thick, sheetGreen] (pd) -- node[lbl, above=1pt]{\texttt{out T}: covariant (returns T)} (pa);
  \node[tn] (cd) at (6.4,0.9) {Setter<Dog>}; \node[tn] (ca) at (9.9,0.9) {Setter<Animal>};
  \draw[->, thick, sheetOrange] (ca) -- node[lbl, above=1pt]{\texttt{in T}: contravariant (takes T)} (cd);
  \node[tn] (bd) at (6.4,0.25) {Box<Dog>}; \node[tn] (ba) at (9.9,0.25) {Box<Animal>};
  \draw[thick, sheetRed, dashed] (bd) -- node[lbl, above=1pt]{\texttt{in out T}: invariant (both)} (ba);
  \node[lbl, anchor=west] at (5.7,-0.25) {variance is \emph{measured}; \texttt{in}/\texttt{out} (4.7) declare + check it};
  % ── 3 mapped type pipeline ──
  \node[pt] at (11.1,2.85) {\textcolor{sheetBlue}{3} a mapped type, step by step};
  \node[sb, minimum width=48mm, anchor=west] (m1) at (11.15,2.25) {\texttt{keyof T} $\to$ \texttt{'id'|'name'|'age'}};
  \node[sb, minimum width=48mm, anchor=west] (m2) at (11.15,1.6) {\texttt{[K in …]} one member per key};
  \node[sb, minimum width=48mm, anchor=west, draw=sheetOrange, fill=sheetOrange!8] (m3) at (11.15,0.95) {\texttt{as} remap: rename, or \texttt{never} = drop};
  \node[sb, minimum width=48mm, anchor=west] (m4) at (11.15,0.3) {\texttt{+/-readonly}, \texttt{+/-?} $\to$ value \texttt{T[K]}};
  \draw[flow] (m1) -- (m2); \draw[flow] (m2) -- (m3); \draw[flow] (m3) -- (m4);
  \node[lbl, anchor=west] at (11.1,-0.25) {\emph{homomorphic} (\texttt{in keyof T}) keeps \texttt{?}/\texttt{readonly}};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item \textbf{Constraint} \texttt{<T extends HasId>} = upper bound, like Swift
        \texttt{T: Protocol} but \emph{structural}; default \texttt{<T = string>}.
        Rule: a type parameter must appear \textbf{twice} (links input to output), else
        use \texttt{unknown}.
  \item \textbf{Inference} collects candidates from each argument; literals widen unless
        \texttt{T extends string} (keeps \texttt{'a'}) or \texttt{<const T>} (5.0 — infers as
        if \texttt{as const}; arrays need a \texttt{readonly unknown[]} constraint).
        \texttt{NoInfer<T>} (5.4) removes one position from inference.
  \item \texttt{keyof T}: union of keys (index signature \texttt{string} gives
        \texttt{string|number}). \texttt{typeof v}: value $\to$ type. \texttt{T[K]}: indexed
        access; \texttt{T[number]} = element type.
  \item \textbf{Conditional} \texttt{T extends U ? X : Y}; \texttt{infer R} captures a piece of
        the match. With \texttt{T} still generic it is \textbf{deferred} — inside a generic
        body you can't narrow it, hence casts in implementations.
  \item \textbf{Template literals} (4.1): \texttt{`on\$\{Capitalize<K>\}`}, cross-product over
        unions, \texttt{infer} parses strings. Intrinsics: \texttt{Uppercase},
        \texttt{Lowercase}, \texttt{Capitalize}, \texttt{Uncapitalize}.
  \item \textbf{Recursion}: \texttt{type Json = … | Json[] | \{[k: string]: Json\}}.
        Tail-recursive conditional types go $\approx$1000 deep (4.5); else error 2589
        ``instantiation is excessively deep and possibly infinite''.
  \item \textbf{Swift}: generics there are specialised/witness-table dispatched, invariant
        (except built-ins), and have no conditional/mapped types. \texttt{some P} $\approx$ a
        generic \texttt{T}; \texttt{any P} $\approx$ plain interface type (no box needed:
        structural).
\end{itemize}

\section{Utility types from scratch (lib.es5.d.ts)}
\begin{lstlisting}[language=TSSheet]
export {};              // module scope: shadows the global names
type Partial<T>  = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, V> = { [P in K]: V };
type Exclude<T, U> = T extends U ? never : T;  // distributes
type Extract<T, U> = T extends U ? T : never;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NonNullable<T> = T & {};                    // since 4.8
type ReturnType<T extends (...a: any) => any> =
  T extends (...a: any) => infer R ? R : any;
type Parameters<T extends (...a: any) => any> =
  T extends (...a: infer P) => any ? P : never;
type Awaited<T> = // simplified: real one handles any thenable
  T extends PromiseLike<infer V> ? Awaited<V> : T;
\end{lstlisting}

\section{Example — what seniors write}
\begin{lstlisting}[language=TSSheet]
type Events = { login: { id: string }; logout: undefined };
type Handlers = {                      // onLogin, onLogout
  [K in keyof Events as `on${Capitalize<string & K>}`]:
    (e: Events[K]) => void };
function get<T, K extends keyof T>(o: T, k: K): T[K] {
  return o[k]; }
type Params<S> = S extends `${string}:${infer P}/${infer R}`
  ? P | Params<`/${R}`>
  : S extends `${string}:${infer P}` ? P : never;
type P1 = Params<'/u/:id/post/:pid'>;  // 'id' | 'pid'
type DeepReadonly<T> = T extends (...a: any[]) => any ? T
  : { readonly [K in keyof T]: DeepReadonly<T[K]> };
function fsm<S extends string>(states: S[], start: NoInfer<S>) {}
fsm(['on', 'off'], 'of');              // error thanks to NoInfer
interface Getter<out T> { get: () => T }
interface Setter<in T>  { set: (v: T) => void }
\end{lstlisting}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{``\texttt{\{a:1\}} is assignable to the constraint of \texttt{T}, but \texttt{T} could be
        instantiated with a different subtype'' — you returned a \emph{new} object where
        the caller's exact \texttt{T} was promised.}
  \trap{\texttt{Omit}/\texttt{keyof} on a \emph{union} keep only common keys:
        distribute first — \texttt{T extends any ? Omit<T, K> : never}.}
  \trap{\texttt{IsNever<T> = T extends never ? …} yields \texttt{never} for
        \texttt{never}: write \texttt{[T] extends [never]}.}
  \trap{Arrays are covariant only through method bivariance: \texttt{Dog[]} as
        \texttt{Animal[]} then \texttt{push(cat)} compiles.}
  \trap{Clever types cost compile time and error readability; prefer overloads or a plain
        union when a call site doesn't gain inference.}
\end{itemize}

\section{Remember}
\emph{Constrain, infer, then compute: conditional = if, mapped = for-each, \texttt{infer} =
pattern bind, recursion = loop.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item Distributive conditional? — a naked \texttt{T} maps over each union member; wrap in
        \texttt{[T]} to stop it.
  \item Covariant vs contravariant? — outputs keep the subtype arrow, inputs flip it,
        both = invariant.
  \item Why \texttt{NoInfer}? — so a default/secondary argument can't widen \texttt{T}.
  \item \texttt{const} type parameter? — literal/readonly inference without \texttt{as
        const} at every call.
  \item Implement \texttt{Pick}/\texttt{ReturnType}? — see the block above.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} ts-type-system ·
ts-config-and-practice · rn-testing-typescript (generic \texttt{List<T>}, nav param lists) ·
Swift generics, \texttt{some}/\texttt{any}, associated types}

\end{document}
