% ts-config-and-practice.tex — TypeScript in a real codebase: tsconfig strict family and
% extra flags, module/moduleResolution, ESM/CJS interop, isolatedModules/verbatimModuleSyntax,
% declaration files + augmentation, brands, enums vs unions, runtime validation at
% boundaries, project references, type-check performance, ts-expect-error, JS->TS migration.
% Source: own knowledge. Senior-interview level.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/typescript/ts-config-and-practice.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=typescript kind=tooling level=senior platform=web new=no round=typescript-2026-09-24 topic=build,tooling,language
% @tags: tsconfig, strict-mode, nouncheckedindexedaccess, moduleresolution, nodenext, verbatimmodulesyntax, isolatedmodules, declaration-files, branded-types, zod, project-references, ts-expect-error
\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,unique,symbol,global,module,var,enum},
  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},
}

\begin{document}

\sheettitle{TypeScript in practice — tsconfig, modules, boundaries}{typescript · memo}

\oneliner{Types are \textbf{erased}: the compiler proves consistency \emph{inside} the program,
so the senior's job is the \textbf{edges} — a strict \texttt{tsconfig}, module settings that
match whoever actually \textbf{loads} the code, \textbf{runtime validation} where data enters,
and a build that stays fast as the codebase grows.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \foreach \x in {5.7,11.0} \draw[sheetGrey!40] (\x,3.0) -- (\x,-0.5);
  % ── 1 two compilers ──
  \node[pt] at (0,2.85) {\textcolor{sheetBlue}{1} who compiles your \texttt{.ts}?};
  \node[sb] (src) at (0.55,1.3) {\texttt{.ts}};
  \node[sb, minimum width=24mm] (tsc) at (2.55,2.2) {\texttt{tsc}: whole program};
  \node[sb, minimum width=24mm, draw=sheetOrange, fill=sheetOrange!8] (bab) at (2.55,0.9) {Babel · esbuild · swc\\Metro: \textbf{one file}};
  \node[sb, minimum width=13mm, draw=sheetGreen, fill=sheetGreen!10] (o1) at (4.85,2.2) {types ✓\\\texttt{.js}+\texttt{.d.ts}};
  \node[sb, minimum width=13mm] (o2) at (4.85,0.9) {\texttt{.js}, types\\\textbf{stripped}};
  \draw[flow] (src) |- (tsc); \draw[flow] (src) |- (bab);
  \draw[flow] (tsc) -- (o1); \draw[flow] (bab) -- (o2);
  \node[lbl, text=sheetOrange, anchor=west] at (0,0.2) {one file at a time can't tell if \texttt{\{Foo\}} is a type $\to$};
  \node[lbl, text=sheetOrange, anchor=west] at (0,-0.02) {\texttt{isolatedModules} / \texttt{verbatimModuleSyntax}: say \texttt{import type}};
  \node[lbl, anchor=west] at (0,-0.28) {RN/Vite: bundler emits, \texttt{tsc -\kern0pt-noEmit} checks in CI};
  % ── 2 the boundary ──
  \node[pt] at (5.8,2.85) {\textcolor{sheetBlue}{2} erased types $\to$ validate at the edge};
  \foreach \src/\y in {HTTP JSON/2.25,storage · MMKV/1.75,deep link · push/1.25,native bridge/0.75} {
    \node[sb, draw=sheetRed, fill=sheetRed!6, minimum width=17mm] at (6.75,\y) {\src};
  }
  \node[sb, minimum width=14mm] (unk) at (8.55,1.5) {\texttt{unknown}};
  \foreach \y in {2.25,1.75,1.25,0.75} \draw[flow] (7.62,\y) -- (unk.west);
  \node[sb, draw=sheetOrange, fill=sheetOrange!10] (par) at (9.55,2.3) {\texttt{parse}\\(zod)};
  \node[sb, draw=sheetGreen, fill=sheetGreen!10, minimum width=15mm] (dom) at (10.2,1.2) {typed core\\\texttt{User}, \texttt{UserId}};
  \draw[flow] (unk) |- (par); \draw[flow] (par) -| (dom);
  \node[lbl, text=sheetRed, anchor=west] at (5.8,0.2) {\texttt{res.json() as User}: a claim, not a check};
  \node[lbl, anchor=west] at (5.8,-0.05) {inside the core: no casts, no \texttt{any},};
  \node[lbl, anchor=west] at (5.8,-0.28) {brands mint only in the parser};
  % ── 3 module resolution ──
  \node[pt] at (11.1,2.85) {\textcolor{sheetBlue}{3} who \emph{loads} the imports?};
  \node[sb, minimum width=22mm] (q) at (13.8,2.3) {runtime resolver?};
  \node[sb, minimum width=22mm, align=left, anchor=north] (nd) at (12.3,1.75) {Node directly\\\texttt{module: nodenext}\\ext.\ required: \texttt{./a.js}\\\texttt{"type"} picks ESM/CJS};
  \node[sb, minimum width=22mm, align=left, anchor=north, draw=sheetGreen, fill=sheetGreen!8] (bu) at (15.3,1.75) {a bundler (Metro,\\Vite, webpack)\\\texttt{moduleResolution:}\\\texttt{bundler}, no ext.};
  \draw[flow] (q) -| (nd); \draw[flow] (q) -| (bu);
  \node[lbl, anchor=west] at (11.1,-0.05) {both honour \texttt{package.json} \texttt{"exports"};};
  \node[lbl, anchor=west] at (11.1,-0.28) {a \emph{library} targets \texttt{nodenext} (works everywhere)};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works — config}
\begin{itemize}\raggedright
  \item \texttt{\textbf{strict}} = \texttt{noImplicitAny}, \texttt{noImplicitThis},
        \texttt{alwaysStrict}, \texttt{strictNullChecks}, \texttt{strictFunctionTypes},
        \texttt{strictBindCallApply}, \texttt{strictPropertyInitialization},
        \texttt{useUnknownInCatchVariables} (4.4), \texttt{strictBuiltinIteratorReturn} (5.6).
        New members join on upgrade — pin TS versions.
  \item \textbf{Not} in strict, worth it: \texttt{noUncheckedIndexedAccess} (\texttt{a[i]}:
        \texttt{T|undefined}), \texttt{exactOptionalPropertyTypes},
        \texttt{noImplicitOverride}, \texttt{noImplicitReturns},
        \texttt{noFallthroughCasesInSwitch}.
  \item \textbf{Modules}: \texttt{nodenext} models Node exactly — \texttt{.mts}/\texttt{.cts}
        and \texttt{"type": "module"} decide ESM vs CJS per file; relative imports name the
        \emph{output} (\texttt{./util.js}; 5.7 \texttt{rewriteRelativeImportExtensions}
        allows \texttt{.ts}). \texttt{bundler} (5.0) + \texttt{module: esnext}/\texttt{preserve}
        (5.4): no extensions. \textbf{Interop}: \texttt{esModuleInterop} emits helpers so
        \texttt{import x from 'cjs'} gets \texttt{module.exports}.
  \item \texttt{\textbf{verbatimModuleSyntax}} (5.0): what you write is what is emitted — \texttt{import type \{A\}} vanishes, \texttt{import \{type A\}} leaves
        \texttt{import \{\} from 'a'} (side effect kept). \texttt{isolatedModules}: forbids what
        a per-file transpiler can't do (type re-exports w/o \texttt{export type}, ambient
        \texttt{const enum}).
  \item \textbf{Declarations}: \texttt{.d.ts} = types only. Untyped lib: \texttt{declare
        module 'x'} (bare = all \texttt{any}) or \texttt{@types/*} (DefinitelyTyped);
        \texttt{"types"} limits auto-included globals; \texttt{skipLibCheck}: faster, hides
        clashes.
  \item \textbf{Project references}: \texttt{composite} + \texttt{references};
        \texttt{tsc -b} rebuilds only stale projects (\texttt{.tsbuildinfo}) and enforces
        layering (Swift: SPM targets).
  \item \textbf{Speed}: \texttt{-\kern0pt-extendedDiagnostics},
        \texttt{-\kern0pt-generateTrace}; interfaces over big \texttt{\&}; annotate exported
        returns (\texttt{isolatedDeclarations} 5.5: parallel \texttt{.d.ts}); tame mega-unions.
        TS 7 = native Go port (previewed 2025).
\end{itemize}

\section{Example — the edges}
\begin{lstlisting}[language=TSSheet]
const User = z.object({ id: z.string(), age: z.number() });
type User = z.infer<typeof User>;      // one source of truth
async function loadUser(res: Response): Promise<User> {
  return User.parse(await res.json()); // throws on bad shape
}
declare const tag: unique symbol;      // brand: nominal, 0 bytes
type UserId = string & { readonly [tag]: 'UserId' };
const toUserId = (s: string) => s as UserId;  // the one cast
const Dir = { Up: 'up', Down: 'down' } as const;
type Dir = (typeof Dir)[keyof typeof Dir];    // 'up' | 'down'
// globals.d.ts -- has export, so it is a module: augment
export {};
declare global { var __APP_VERSION__: string }
// assets.d.ts -- no import/export: a script, ambient modules
declare module '*.png' { const id: number; export default id; }
\end{lstlisting}

\section{Enums vs unions}
\begin{itemize}\raggedright
  \item \texttt{enum} emits a runtime object; numeric ones add a reverse map
        (\texttt{Object.values} gives names \emph{and} numbers); a string enum rejects the
        bare literal \texttt{'up'} — nominal-ish.
  \item \texttt{const enum} inlines, but breaks per-file transpilers and package
        boundaries. 5.8 \texttt{erasableSyntaxOnly} bans enums, namespaces, parameter
        properties (Node type stripping). Default: literal union / \texttt{as const}.
\end{itemize}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{\texttt{JSON.parse} and \texttt{Response.json()} return \texttt{any}: the lie spreads
        silently. Type the result \texttt{unknown}, then parse.}
  \trap{\texttt{catch (e)} is \texttt{unknown} under strict: \texttt{e instanceof Error}
        before \texttt{e.message}.}
  \trap{\texttt{paths} aliases are \emph{not} rewritten in output — Metro/Jest/Node need the
        same alias (\texttt{babel-plugin-module-resolver}, \texttt{moduleNameMapper}).}
  \trap{\texttt{declare global} in a file with no \texttt{import}/\texttt{export} is an
        error: add \texttt{export \{\}}.}
  \trap{\texttt{@ts-ignore} hides every future error on that line; \texttt{@ts-expect-error}
        (3.9) \emph{fails} once the error is gone — self-cleaning.}
\end{itemize}

\section{JS $\to$ TS migration}
{\raggedright\texttt{allowJs} (mixed build) $\to$ \texttt{checkJs}/\texttt{// @ts-check} + JSDoc $\to$
rename leaves first (utils, API client) upward; type the \textbf{boundaries} first — most value.
Turn \texttt{strict} on early with a \texttt{@ts-expect-error} baseline (or a stricter tsconfig
for converted dirs); lint \texttt{no-explicit-any}, track the count down.\par}

\section{Remember}
\emph{Strict inside · parse at the edge · config matches the loader.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item \texttt{node16/nodenext} vs \texttt{bundler}? — who resolves imports at runtime.
  \item Why \texttt{verbatimModuleSyntax}? — per-file transpilers can't know what is a type.
  \item Nominal IDs? — a brand: \texttt{string \& \{[tag]: 'UserId'\}}.
  \item Why validate if we have types? — they're erased; the server/storage can lie.
  \item Slow \texttt{tsc}? — trace it, split with project references, annotate exported
        returns, shrink mega-unions.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} ts-type-system ·
ts-generics-advanced · rn-testing-typescript (\texttt{strict}, Codegen spec types) ·
Swift modules \& access control}

\end{document}
