Type safety
Skill jacob-balslev/skill-graph/marketplace/skills/type-safety
Skills that know your codebase. Repo-grounded, contract-validated, agent-routable.
npx -y skills add jacob-balslev/skill-graph --skill type-safetyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when reasoning about types as a quality property of code: what guarantees the type system actually provides, the difference between sound and unsound systems, structural vs nominal typing, type narrowing and exhaustiveness, the runtime/compile-time boundary, and where validation must happen because the type system cannot. Covers TypeScript, Flow, Hindley-Milner languages, and gradual typing in general. Do NOT use for runtime input validation library choice (use api-design for API surface validation; use individual library docs for library mechanics), for SQL type mapping (use entity-relationship-modeling), or for type system implementation (compilers — out of scope). Do NOT use for implement HMAC verification for an inbound webhook (use webhook-integration). Do NOT use for design the JSON shape of an API endpoint (use api-design).
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
18.8 KB, ~3.5k tokens by cl100k_base, as published. Nobody here has run it
Type Safety
Concept of the skill
Two-layer model: a compile-time layer where the type checker verifies internal consistency (claims about values are coherent across the codebase) and a runtime layer where values from outside the program (HTTP responses, environment variables, parsed JSON, untrusted user input) have NO type until parsed, regardless of what type annotation sits next to them. The discipline is the boundary contract between these two layers: validate at I/O boundaries (parse, never assert), then trust the type inside. Layered on top: soundness (does the system rule out ALL type errors of the kinds it tracks, or only some via escape hatches?), structural vs nominal typing (do shapes match by structure or by name?), narrowing (refine a broad type by control-flow evidence), and exhaustiveness checking (force the compiler to flag uncovered cases in a discriminated union).
Distinguishes a syntactic claim from a semantic guarantee. Without type-safety as a discipline (not just a compiler flag), JSON.parse(x) as User looks identical to a validated parse — the cast is decoration, not verification. The alternative — "the compiler said it was fine, so it's fine" — fails because gradual systems like TypeScript are unsound by design (escape hatches: any, as, function bivariance, ambient declarations) and untrusted input arrives un-typed regardless of what annotation sits next to it. Type-safety replaces the "compiler-blessed" mental model with "compile-time guarantees stop at the I/O boundary, runtime validation takes over there."
Distinct from api-design, which owns the external request/response surface shape — type-safety owns the discipline of expressing internal program correctness as types, and where the type system stops at the boundary api-design defines. Distinct from testing-strategy, which owns runtime verification of behavior — type-safety owns compile-time verification of structure, and the two cover different failure modes (a function can be type-safe and behaviorally wrong, or behaviorally correct and type-unsafe). Distinct from entity-relationship-modeling, which owns persistence and entity shape — type-safety owns the in-memory type contracts that consume that shape. Distinct from input validation libraries (Zod, Yup, io-ts), which provide the runtime parsing mechanism — type-safety is the discipline that decides where parsing must happen because the type system cannot. Type safety is to programs what a passport check is to international travel — the document (type annotation) certifies identity within the issuing country's records, but on the way through customs (the I/O boundary), the document is re-verified against the actual traveler, and any mismatch is rejected before they enter the trusted zone. The wrong mental model is that a TypeScript as cast is a form of validation. It is not. The cast is a programmer-asserted claim that compiles unchecked at runtime, and JSON.parse(x) as User produces a value typed as User with zero verification that it actually has the fields a User must have. The misconception conflates two layers — the compile-time claim (which the compiler accepts) and the runtime guarantee (which the cast does nothing to establish). The discipline is to use runtime validators (Zod, io-ts, manual parse functions) at every I/O boundary and treat as as an explicit, justified, rare escape hatch — not as the default way to silence a type error.
Coverage
The discipline of using a type system to rule out classes of runtime errors before they occur. Covers what soundness means and where TypeScript (and other gradual systems) is unsound, structural vs nominal typing, type narrowing and exhaustiveness checking, the runtime boundary problem, the difference between any and unknown, when to use type assertions (rarely) and when to validate (always at I/O boundaries), and the connection to runtime validation libraries.
Philosophy of the skill
Types are claims about values; type-checking is proof-checking. A program that compiles is a program whose claims have been internally consistent — but a program is more than its compiler. Values that arrive from outside the program (HTTP responses, environment variables, parsed JSON, untrusted user input) have no type until you parse them, no matter what type annotation sits next to them.
The discipline of type-safety is to take the compile-time guarantees seriously and to know exactly where they stop. A codebase that pretends JSON.parse(x) as User is safe has confused a syntactic claim with a semantic guarantee. A codebase that validates at the boundary and trusts the type inside has correctly aligned the two layers.
In gradual systems like TypeScript, the discipline includes treating escape hatches (any, as, // @ts-ignore) as exceptional, justified, and rare — not as the default response to a type error.
Soundness — What the System Actually Promises
| System | Soundness | Where it leaks |
|---|---|---|
| TypeScript | Unsound (by design) | any, as, function bivariance, ambient declarations, type assertions, Object.keys() typed as string[], array .find() returning a T not T | undefined (without strict flag), unchecked noUncheckedIndexedAccess, mutable arrays in covariant positions |
| Flow | Unsound | Similar escape hatches; less broadly adopted |
| Python + mypy | Unsound (gradual) | Any, cast, dynamic-only constructs |
| Java | Sound for types, unsound for null | NullPointerException; generics erased at runtime |
| C# | Mostly sound | Reflection, dynamic |
| Go | Sound for types, structural interfaces | Empty interface (interface{}) is the escape hatch; type assertions panic on failure |
| Rust | Sound (memory + types) | unsafe blocks are documented escape hatches |
| Haskell | Sound (within purity) | unsafePerformIO, FFI |
Practical TypeScript stance: Enable strict mode (strict: true), enable noUncheckedIndexedAccess, enable noImplicitAny. These flags close the most common leakage points. Without them, the system's guarantees are substantially weaker than developers assume.
Narrowing
Narrowing is the type checker's mechanism for refining a broad type based on control-flow evidence. Use it instead of casts.
function process(x: string | number | null) {
if (x === null) return; // narrows to string | number
if (typeof x === 'string') { // narrows to string
return x.toUpperCase();
}
// here x is narrowed to number
return x.toFixed(2);
}
| Narrowing technique | Use when |
|---|---|
typeof x === '...' | Distinguishing primitives |
x instanceof Class | Distinguishing class instances |
'field' in x | Distinguishing discriminated objects |
x === literal | Distinguishing literal types |
| Discriminated union via tag field | Designed-in narrowing for ADTs |
User-defined type guards (x is T) | Custom predicates |
Array.isArray(x) | Array vs non-array |
Discriminated unions are the strongest pattern:
type Result =
| { ok: true; value: User }
| { ok: false; error: string };
function handle(r: Result) {
if (r.ok) return r.value; // narrows; `r.error` is not accessible here
return r.error; // narrowed to the error branch
}
Exhaustiveness Checking
Force the compiler to verify that all cases of a union are handled. The pattern uses an unreachable never branch.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
function describe(m: Method): string {
switch (m) {
case 'GET': return 'safe';
case 'POST': return 'mutation';
case 'PUT': return 'idempotent replacement';
case 'DELETE': return 'idempotent removal';
default: {
const _exhaustive: never = m; // compile error if a case is missing
throw new Error(`unhandled: ${_exhaustive}`);
}
}
}
When a new variant is added to Method, the never assignment fails to type-check — the compiler points at every missing branch. This converts a runtime "unhandled case" bug into a compile-time error.
The Runtime Boundary
Type information stops at the runtime boundary. Values crossing in must be parsed; values crossing out are serialized.
| Boundary | Risk | Discipline |
|---|---|---|
JSON.parse(networkResponse) | Returns any (or unknown with stricter typing); no validation | Parse with a schema (Zod, io-ts) before trusting the type |
process.env.X | All env vars are string | undefined, but TypeScript may type them as string via globals | Validate at startup with a typed env config object |
localStorage.getItem(k) | Returns string | null, but the contents are untyped | Parse + validate before use |
fetch(url).then(r => r.json()) | The promise resolves with any | Validate against an expected schema |
| Database driver results | Library-typed; trust depends on the library's contract | Verify the library actually checks types at the boundary |
Function(string) / eval | Arbitrary code; arbitrary types | Avoid; if necessary, type the result as unknown |
The pattern: validate at the boundary; trust the type inside.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const raw = await fetch(`/api/users/${id}`).then(r => r.json());
return UserSchema.parse(raw); // throws on validation failure
}
// Inside the rest of the program, User is trusted.
any vs unknown vs never
| Type | Set of values | What you can do with it |
|---|---|---|
any | All values | Anything (escape hatch — no checking) |
unknown | All values | Nothing until you narrow (safe escape hatch) |
never | No values | Nothing (used for exhaustiveness checks and unreachable code) |
void | Returned, not consumed | Function return only; the value is "no value worth using" |
Rule: prefer unknown over any always. The cost is one narrowing step; the benefit is type-safety preserved.
Verification
After applying this skill, verify:
- TypeScript strict mode is enabled (
"strict": truein tsconfig). -
noUncheckedIndexedAccessis enabled; array/object access producesT | undefined. - No
anyappears in committed code without an inline comment explaining whyunknownis insufficient. - No
as Typecast appears without an inline comment explaining why narrowing is insufficient. - Every I/O boundary parses with a runtime validator (Zod, io-ts, valibot, etc.) before the value is treated as typed.
- Discriminated unions have an exhaustiveness check at every consumer site.
- Public API boundaries (exported functions, route handlers, library entry points) have explicit return types — not just inferred.
-
// @ts-ignoreand// @ts-expect-errorare accompanied by a justification and a tracking comment.
Do NOT Use When
| Instead of this skill | Use | Why |
|---|---|---|
| Designing the JSON shape of an API endpoint | api-design | api-design owns the external surface contract; this skill owns internal type discipline |
| Verifying behavior at runtime with tests | testing-strategy | testing-strategy owns runtime verification; this skill owns compile-time |
| Designing database schema and column types | entity-relationship-modeling | entity-relationship-modeling owns persistence shape; this skill owns in-memory type contracts |
| Choosing between Zod / io-ts / valibot | individual library docs + api-design | Library choice is a tactical decision below this skill |
| Implementing the compiler / type-checker | language compiler implementation references | Out of scope — this skill is about using type systems, not building them |
Key Sources
- Pierce, B. C. (2002). Types and Programming Languages. MIT Press. The canonical textbook. Chapters 1-3 cover untyped lambda calculus, simple types, and the soundness/progress/preservation framework that underpins every type system.
- Siek, J. G., & Taha, W. (2006). "Gradual Typing for Functional Languages." Scheme and Functional Programming 2006. The original gradual typing paper.
- Siek, J. G., & Vachharajani, M. (2008). "Gradual typing with unification-based inference." Proceedings of the 2008 symposium on Dynamic languages. Formalizes the soundness vs ergonomics trade-off in gradual systems.
- Microsoft. TypeScript Handbook. The reference for TypeScript's type system, including the documented unsoundness in Type Compatibility and Narrowing chapters.
- Anders Hejlsberg et al. TypeScript Design Goals. Explicit acknowledgement that TypeScript trades soundness for ergonomics: "non-goals: apply a sound or 'provably correct' type system."
- Curry, H. B., & Feys, R. (1958). Combinatory Logic, Volume I. Original work on the Curry-Howard correspondence — types as propositions, programs as proofs.
Skill Graph context
<!-- skill-graph-context:start (generated — do not edit by hand) -->Classification
- Subject:
quality-assurance - Public:
true - Domain:
quality/types - Scope: Use when reasoning about types as a quality property of code: what guarantees the type system actually provides, the difference between sound and unsound systems, structural vs nominal typing, type narrowing and exhaustiveness, the runtime/compile-time boundary, and where validation must happen because the type system cannot. Covers TypeScript, Flow, Hindley-Milner languages, and gradual typing in general. Do NOT use for runtime input validation library choice (use api-design for API surface validation; use individual library docs for library mechanics), for SQL type mapping (use entity-relationship-modeling), or for type system implementation (compilers — out of scope).
When to use
- review whether this discriminated union has an exhaustiveness check at the switch
- decide whether to use
anyorunknownfor this third-party JSON payload - explain why TypeScript's
ascast doesn't actually validate at runtime - design where Zod (or any validator) parses at the application boundary
- Triggers:
is this type-safe,should this beanyorunknown``,exhaustiveness check,narrowing,where does validation belong
Not for
- implement HMAC verification for an inbound webhook (use webhook-integration)
- design the JSON shape of an API endpoint (use api-design)
- choose between Postgres column types (use entity-relationship-modeling)
Related skills
- Verify with:
code-review,testing-strategy,client-server-boundary - Related:
entity-relationship-modeling,api-design,testing-strategy,code-review,prompt-injection-defense
Concept
- Mental model: |
- Purpose: |
- Boundary: |
- Analogy: Type safety is to programs what a passport check is to international travel — the document (type annotation) certifies identity within the issuing country's records, but on the way through customs (the I/O boundary), the document is re-verified against the actual traveler, and any mismatch is rejected before they enter the trusted zone.
- Common misconception: |
Keywords
type safety,TypeScript,sound type system,unsound type system,structural typing,nominal typing,type narrowing,exhaustiveness check,gradual typing,runtime boundary