{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"types-as-always-on-oracles","title":"The type checker got more valuable","url":"https://addaly.com/learn/how-software-changed/types-as-always-on-oracles","position":23,"of":87,"minutes":9,"answer":"A type checker is the only reviewer whose capacity scales with the amount of code, and it earns most when types carry meaning rather than shape, so identifiers.","keyIdea":"A type checker is the only reviewer whose capacity scales with the amount of code, and it earns most when types carry meaning rather than shape, so identifiers, units and currencies become mistakes the compiler refuses."},"body":{"format":"markdown","text":"## An oracle that runs on every line, for ever, for nothing\n\nA test checks the cases you wrote. A type checks every call site that exists and every one anyone writes later, including the ones produced at four in the morning by something that has never seen your codebase.\n\nThat asymmetry always favoured types. It favours them much more now, because the volume of code entering your repository went up and the proportion of it read carefully went down. A strict type checker is the only reviewer that scales linearly with the amount of code and never gets tired.\n\n## Turn on the settings that actually bite\n\nMost projects have a type checker configured at a strictness where it catches almost nothing. The defaults are lenient on purpose, so adoption is easy. Adoption is not the goal.\n\nTypeScript, in `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitOverride\": true,\n    \"noFallthroughCasesInSwitch\": true\n  }\n}\n```\n\n`noUncheckedIndexedAccess` is the one people skip and the one that pays. Without it, `arr[10]` is typed as the element type even when the array has three items, and every off-by-one becomes a runtime error instead of a compile error.\n\nPython, with mypy or pyright:\n\n```ini\n[mypy]\nstrict = True\nwarn_unreachable = True\ndisallow_any_explicit = True\n```\n\nThen the rule that makes it stick: `Any` and `any` require a comment explaining why, and `# type: ignore` requires the specific error code, not a bare ignore. A bare ignore silences everything on that line for ever, including errors introduced next year.\n\n## Make the types carry meaning, not just shape\n\nThis is where the real defect prevention is, and it costs almost nothing.\n\nA function that takes `(float, float, str)` accepts arguments in the wrong order silently. A function that takes `(Money, Rate, CurrencyCode)` does not.\n\n```typescript\ntype Cents = number & { readonly __brand: \"Cents\" };\ntype UserId = string & { readonly __brand: \"UserId\" };\ntype OrderId = string & { readonly __brand: \"OrderId\" };\n\n// passing a UserId where an OrderId is required is now a compile error,\n// and it costs zero at runtime\n```\n\nPython gets the same with `typing.NewType`, Rust with the newtype struct, Java with a small value class. Units, identifiers, currencies, timezone-aware versus naive datetimes, sanitised versus raw strings: every one of these is a category of bug that a type can make unrepresentable.\n\nThe Mars Climate Orbiter was lost in 1999 because one team worked in pound-seconds and another in newton-seconds. A newtype would have caught it at compile time. Every system that handles money in more than one currency has that bug waiting somewhere.\n\n## Parse, do not validate\n\nA validation function returns a boolean and throws the information away. A parsing function returns a new type that carries the guarantee.\n\n```python\n# validation: the guarantee evaporates immediately\nif is_valid_email(s):\n    send(s)          # nothing stops send() being called with anything\n\n# parsing: the guarantee travels with the value\naddr = EmailAddress.parse(s)   # raises, or returns EmailAddress\nsend(addr)                     # send() accepts only EmailAddress\n```\n\nThe consequence is that the check happens once, at the boundary, and every function downstream is relieved of re-checking, and, more to the point, cannot forget to. This one pattern removes a large fraction of defensive null checks and the bugs that live in the gaps between them.\n\n## Exhaustiveness is the check that survives change\n\n```typescript\nfunction label(s: Status): string {\n  switch (s) {\n    case \"draft\": return \"Draft\";\n    case \"sent\": return \"Sent\";\n    case \"paid\": return \"Paid\";\n    default: {\n      const _exhaustive: never = s;   // compile error if a status is added\n      return _exhaustive;\n    }\n  }\n}\n```\n\nWhen someone adds `\"refunded\"` to `Status` next quarter, this fails to compile and so does every other exhaustive switch in the codebase. That is a list of every place that needs attention, generated automatically, at the exact moment it becomes true. Rust's `match` and Python's `assert_never` do the same job.\n\n## The honest costs\n\nStrict typing is not free and people who claim it is lose the argument with anyone who has done it.\n\n- Retrofitting strictness onto a large untyped codebase is weeks, not days, and the middle of that migration is unpleasant.\n- Type errors at the boundaries of dynamic data (JSON from an API, a database row, a YAML config) are where most of the friction lives. The answer is a parsing layer at each boundary, which is more code.\n- Some type-level constructions cost more in complexity than the bugs they prevent. If a signature needs three lines of generics, ask whether the simpler design was available.\n- Types cannot express most of what matters. \"The discount is applied before tax\" is not a type.\n\nWhat makes the trade worth it in the current environment: a type checker rejects wrong code without anyone reading it, and that is now the constrained resource."},"check":{"question":"A codebase has `strict: true` in TypeScript but represents every identifier as `string` and every money amount as `number`. What class of defect remains completely undetected?","options":[{"text":"Null and undefined dereferences, which strict mode only partially covers when values cross module boundaries.","correct":false,"tempting":"Strict mode's null checking is exactly what does cover this; naming it as the gap inverts what the setting does."},{"text":"Arguments of the same primitive type passed in the wrong order or the wrong unit.","correct":true},{"text":"Runtime shape mismatches from external JSON, since the compiler cannot verify data it has not seen at build time.","correct":false,"tempting":"A genuine gap in all static typing, but it is unrelated to how identifiers and money are modelled inside the system."},{"text":"Exhaustiveness failures when a new variant is added to a union, because strict mode does not enable exhaustive switch checking by default.","correct":false,"tempting":"Conflates two settings; exhaustiveness needs an explicit never assignment, but that is a separate concern from primitive obsession."}]},"prev":{"slug":"differential-testing","title":"The old version is your best oracle","url":"https://addaly.com/learn/how-software-changed/differential-testing"},"next":{"slug":"constraints-in-the-database","title":"The database is the last honest gate","url":"https://addaly.com/learn/how-software-changed/constraints-in-the-database"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The type checker got more valuable', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/types-as-always-on-oracles (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The type checker got more valuable","courseTitle":"How Software Development Changed","module":{"index":3,"slug":"oracles-and-verification","title":"Building oracles that stay ahead"},"answer":null,"keyIdea":"A type checker is the only reviewer whose capacity scales with the amount of code, and it earns most when types carry meaning rather than shape, so identifiers, units and currencies become mistakes the compiler refuses.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"an-oracle-that-runs-on-every-line-for-ever-for-nothing","runs":[{"text":"An oracle that runs on every line, for ever, for nothing"}]},{"t":"p","runs":[{"text":"A test checks the cases you wrote. A type checks every call site that exists and every one anyone writes later, including the ones produced at four in the morning by something that has never seen your codebase."}]},{"t":"p","runs":[{"text":"That asymmetry always favoured types. It favours them much more now, because the volume of code entering your repository went up and the proportion of it read carefully went down. A strict type checker is the only reviewer that scales linearly with the amount of code and never gets tired."}]},{"t":"h2","id":"turn-on-the-settings-that-actually-bite","runs":[{"text":"Turn on the settings that actually bite"}]},{"t":"p","runs":[{"text":"Most projects have a type checker configured at a strictness where it catches almost nothing. The defaults are lenient on purpose, so adoption is easy. Adoption is not the goal."}]},{"t":"p","runs":[{"text":"TypeScript, in "},{"text":"tsconfig.json","code":true},{"text":":"}]},{"t":"code","lang":"json","label":"JSON","text":"{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitOverride\": true,\n    \"noFallthroughCasesInSwitch\": true\n  }\n}"},{"t":"p","runs":[{"text":"noUncheckedIndexedAccess","code":true},{"text":" is the one people skip and the one that pays. Without it, "},{"text":"arr[10]","code":true},{"text":" is typed as the element type even when the array has three items, and every off-by-one becomes a runtime error instead of a compile error."}]},{"t":"p","runs":[{"text":"Python, with mypy or pyright:"}]},{"t":"code","lang":"ini","label":"INI","text":"[mypy]\nstrict = True\nwarn_unreachable = True\ndisallow_any_explicit = True"},{"t":"p","runs":[{"text":"Then the rule that makes it stick: "},{"text":"Any","code":true},{"text":" and "},{"text":"any","code":true},{"text":" require a comment explaining why, and "},{"text":"# type: ignore","code":true},{"text":" requires the specific error code, not a bare ignore. A bare ignore silences everything on that line for ever, including errors introduced next year."}]},{"t":"h2","id":"make-the-types-carry-meaning-not-just-shape","runs":[{"text":"Make the types carry meaning, not just shape"}]},{"t":"p","runs":[{"text":"This is where the real defect prevention is, and it costs almost nothing."}]},{"t":"p","runs":[{"text":"A function that takes "},{"text":"(float, float, str)","code":true},{"text":" accepts arguments in the wrong order silently. A function that takes "},{"text":"(Money, Rate, CurrencyCode)","code":true},{"text":" does not."}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"type Cents = number & { readonly __brand: \"Cents\" };\ntype UserId = string & { readonly __brand: \"UserId\" };\ntype OrderId = string & { readonly __brand: \"OrderId\" };\n\n// passing a UserId where an OrderId is required is now a compile error,\n// and it costs zero at runtime"},{"t":"p","runs":[{"text":"Python gets the same with "},{"text":"typing.NewType","code":true},{"text":", Rust with the newtype struct, Java with a small value class. Units, identifiers, currencies, timezone-aware versus naive datetimes, sanitised versus raw strings: every one of these is a category of bug that a type can make unrepresentable."}]},{"t":"p","runs":[{"text":"The Mars Climate Orbiter was lost in 1999 because one team worked in pound-seconds and another in newton-seconds. A newtype would have caught it at compile time. Every system that handles money in more than one currency has that bug waiting somewhere."}]},{"t":"h2","id":"parse-do-not-validate","runs":[{"text":"Parse, do not validate"}]},{"t":"p","runs":[{"text":"A validation function returns a boolean and throws the information away. A parsing function returns a new type that carries the guarantee."}]},{"t":"code","lang":"python","label":"Python","text":"# validation: the guarantee evaporates immediately\nif is_valid_email(s):\n    send(s)          # nothing stops send() being called with anything\n\n# parsing: the guarantee travels with the value\naddr = EmailAddress.parse(s)   # raises, or returns EmailAddress\nsend(addr)                     # send() accepts only EmailAddress"},{"t":"p","runs":[{"text":"The consequence is that the check happens once, at the boundary, and every function downstream is relieved of re-checking, and, more to the point, cannot forget to. This one pattern removes a large fraction of defensive null checks and the bugs that live in the gaps between them."}]},{"t":"h2","id":"exhaustiveness-is-the-check-that-survives-change","runs":[{"text":"Exhaustiveness is the check that survives change"}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"function label(s: Status): string {\n  switch (s) {\n    case \"draft\": return \"Draft\";\n    case \"sent\": return \"Sent\";\n    case \"paid\": return \"Paid\";\n    default: {\n      const _exhaustive: never = s;   // compile error if a status is added\n      return _exhaustive;\n    }\n  }\n}"},{"t":"p","runs":[{"text":"When someone adds "},{"text":"\"refunded\"","code":true},{"text":" to "},{"text":"Status","code":true},{"text":" next quarter, this fails to compile and so does every other exhaustive switch in the codebase. That is a list of every place that needs attention, generated automatically, at the exact moment it becomes true. Rust’s "},{"text":"match","code":true},{"text":" and Python’s "},{"text":"assert_never","code":true},{"text":" do the same job."}]},{"t":"h2","id":"the-honest-costs","runs":[{"text":"The honest costs"}]},{"t":"p","runs":[{"text":"Strict typing is not free and people who claim it is lose the argument with anyone who has done it."}]},{"t":"ul","items":[[{"text":"Retrofitting strictness onto a large untyped codebase is weeks, not days, and the middle of that migration is unpleasant."}],[{"text":"Type errors at the boundaries of dynamic data (JSON from an API, a database row, a YAML config) are where most of the friction lives. The answer is a parsing layer at each boundary, which is more code."}],[{"text":"Some type-level constructions cost more in complexity than the bugs they prevent. If a signature needs three lines of generics, ask whether the simpler design was available."}],[{"text":"Types cannot express most of what matters. “The discount is applied before tax” is not a type."}]]},{"t":"p","runs":[{"text":"What makes the trade worth it in the current environment: a type checker rejects wrong code without anyone reading it, and that is now the constrained resource."}]}],"check":{"question":[{"text":"A codebase has "},{"text":"strict: true","code":true},{"text":" in TypeScript but represents every identifier as "},{"text":"string","code":true},{"text":" and every money amount as "},{"text":"number","code":true},{"text":". What class of defect remains completely undetected?"}],"options":[{"text":[{"text":"Null and undefined dereferences, which strict mode only partially covers when values cross module boundaries."}],"correct":false,"tempting":[{"text":"Strict mode’s null checking is exactly what does cover this; naming it as the gap inverts what the setting does."}]},{"text":[{"text":"Arguments of the same primitive type passed in the wrong order or the wrong unit."}],"correct":true,"tempting":null},{"text":[{"text":"Runtime shape mismatches from external JSON, since the compiler cannot verify data it has not seen at build time."}],"correct":false,"tempting":[{"text":"A genuine gap in all static typing, but it is unrelated to how identifiers and money are modelled inside the system."}]},{"text":[{"text":"Exhaustiveness failures when a new variant is added to a union, because strict mode does not enable exhaustive switch checking by default."}],"correct":false,"tempting":[{"text":"Conflates two settings; exhaustiveness needs an explicit never assignment, but that is a separate concern from primitive obsession."}]}],"order":[2,3,1,0],"why":null},"prev":{"slug":"differential-testing","title":"The old version is your best oracle","minutes":10},"next":{"slug":"constraints-in-the-database","title":"The database is the last honest gate","minutes":9}}}