{"article":{"slug":"your-type-guard-can-silently-drift-from-your-typescript-type","title":"Your Type Guard Can Silently Drift from Your TypeScript Type","subtitle":null,"summary":"TypeScript type guards can quietly diverge from the type they claim to check—rejecting valid data loudly or accepting invalid data silently. Why drift happens and how to keep guards in sync.","content_type":"tutorial","language":"en","canonical_url":"https://dev.to/nyaomaru/your-type-guard-can-silently-drift-from-your-typescript-type-o57","author":{"name":"nyaomaru","url":"https://dev.to/nyaomaru","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"DEV Community","url":"https://dev.to/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Web Development","slug":"web-development","url":"https://listedarticles.com/topics/web-development"},{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1512,"reading_minutes":7,"published_at":"2026-09-24T00:00:00.000Z","added_at":"2026-09-25T09:16:56.511Z","updated_at":"2026-09-25T09:16:56.511Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/your-type-guard-can-silently-drift-from-your-typescript-type","markdown_url":"https://listedarticles.com/articles/your-type-guard-can-silently-drift-from-your-typescript-type.md","example":false,"citation":"nyaomaru, DEV Community. \"Your Type Guard Can Silently Drift from Your TypeScript Type.\" 24 Sept 2026. https://dev.to/nyaomaru/your-type-guard-can-silently-drift-from-your-typescript-type-o57 (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://dev.to/nyaomaru/your-type-guard-can-silently-drift-from-your-typescript-type-o57"},"body_markdown":"# Your Type Guard Can Silently Drift from Your TypeScript Type\n\nHoi hoi! 👋\n\nI'm @nyaomaru, a frontend engineer just back from a short vacation on Texel, a small island in the Netherlands. 😸🏝️\n\nToday, let's talk about a type guard that looks completely safe.\n\n\n```\nconst isUser = (value: unknown): value is User => {\n  // runtime checks...\n};\n```\nLooks good, right?\n\nTypeScript knows that when `isUser(value)` returns `true`, the value is a `User`.\n\nBut there's a small problem\n\nTypeScript trusts that promise.\n\n\nIt doesn't prove that your runtime checks actually validate every field in `User`.\n\nAnd that's where type guards can slowly drift away from the types they claim to protect.\n\nLet's take a look! 👀\n\n## 🕳️ A Type Guard Can Become Outdated Without an Error\n\nImagine we start with this type\n\n\n```\ntype User = {\n  id: string;\n  name: string;\n};\n```\nAnd a hand-written type guard\n\n\n```\nconst isUser = (value: unknown): value is User => {\n  if (typeof value !== \"object\" || value === null) {\n    return false;\n  }\n  const candidate = value as Record<string, unknown>;\n  return typeof candidate.id === \"string\" && typeof candidate.name === \"string\";\n};\n```\nSo far, everything matches.\n\nLater, we update `User`\n\n\n```\ntype User = {\n  id: string;\n  name: string;\n  role: \"admin\" | \"member\";\n};\n```\nBut we forget to update the guard.\n\n\n```\nconst isUser = (value: unknown): value is User => {\n  if (typeof value !== \"object\" || value === null) {\n    return false;\n  }\n  const candidate = value as Record<string, unknown>;\n  return typeof candidate.id === \"string\" && typeof candidate.name === \"string\";\n};\n```\nThere is no `role` check.\n\nBut this still compiles. 😿\n\n## 🧠 Why Doesn't TypeScript Catch This?\n\nBecause this\n\n\n```\n(value: unknown): value is User\n```\nis a user-defined type predicate.\n\nYou are telling TypeScript\n\nTrust me. If this function returns `true`, the value is a `User`.\n\n\nTypeScript can check whether the declared predicate type itself makes sense.\n\nBut it cannot generally prove that arbitrary runtime logic actually validates every part of that type.\n\nSo this is possible\n\n\n```\nconst isUser = (_value: unknown): _value is User => true;\n```\nTerrible guard.\n\nPerfectly valid TypeScript. 😹\n\nThe return type is a contract written by us, not a proof generated from the function body.\n\n## 🔄 This Becomes a Maintenance Problem\n\nThe annoying part isn't writing the guard once.\n\nIt's keeping these two things synchronized over time\n\n\n```\nTypeScript type\n      ↕\nRuntime validation\n```\nTypes change.\n\nProperties get\n\n- added\n- removed\n- renamed\n- made optional\n- changed to another type\n\nAnd every time that happens, we need to remember that some runtime guard somewhere may also need an update.\n\nIf we forget, the compiler may not tell us.\n\nThat's the kind of bug I really don't want to rely on memory to prevent.\n\n## ✅ What If the Type Could Be the Contract?\n\nThis is one of the reasons I added `typedStruct` to `is-kit`.\n\nSuppose the application type already exists:\n\n\n```\ntype User = {\n  id: string;\n  name: string;\n  age?: number;\n};\n```\nWe can build the guard against that existing type\n\n\n```\nimport { isNumber, isString, optionalKey, typedStruct } from \"is-kit\";\nconst isUser = typedStruct<User>()({\n  id: isString,\n  name: isString,\n  age: optionalKey(isNumber),\n});\n```\nNow the field map has a type-level relationship with `User`.\n\nAt runtime, it still performs ordinary object validation.\n\nBut at compile time, TypeScript can check whether the guards we declared match the object type they're supposed to follow.\n\n## 💥 Now Drift Becomes Visible\n\nLet's add a field again\n\n\n```\ntype User = {\n  id: string;\n  name: string;\n  role: \"admin\" | \"member\";\n  age?: number;\n};\n```\nBut forget to update the guard\n\n\n```\ntypedStruct<User>()({\n  id: isString,\n  name: isString,\n  age: optionalKey(isNumber),\n  // TypeScript error:\n  // role is missing\n});\n```\nNice.\n\nThe runtime bug became a compile-time problem.\n\nThe same thing happens if the guard uses an incompatible field type\n\n\n```\nimport {\n  isNumber,\n  isString,\n  oneOfValues,\n  optionalKey,\n  typedStruct,\n} from \"is-kit\";\ntypedStruct<User>()({\n  id: isString,\n  name: isNumber,\n  // TypeScript error:\n  // User[\"name\"] is string\n  role: oneOfValues(\"admin\", \"member\"),\n  age: optionalKey(isNumber),\n});\n```\nThis is the part I care about most.\n\n`typedStruct` doesn't eliminate maintenance.\n\nIt makes forgotten maintenance visible.\n\n## 🧩 Optional and Nullable Are Different\n\nAnother place where object guards can get confusing is optional properties.\n\nConsider:\n\n\n```\ntype User = {\n  id: string;\n  nickname?: string | null;\n};\n```\nThere are two separate ideas here\n\n\n```\nnickname may be absent\n```\nand\n\n\n```\nnickname may exist with the value null\n```\nThose are different runtime contracts.\n\nWith `typedStruct`\n\n\n```\nimport { isString, nullable, optionalKey, typedStruct } from \"is-kit\";\nconst isUser = typedStruct<User>()({\n  id: isString,\n  nickname: optionalKey(nullable(isString)),\n});\n```\nNow\n\n\n```\nisUser({ id: \"user-1\" });\n// true\nisUser({\n  id: \"user-1\",\n  nickname: null,\n});\n// true\nisUser({\n  id: \"user-1\",\n  nickname: \"Neko\",\n});\n// true\nisUser({\n  id: \"user-1\",\n  nickname: 42,\n});\n// false\n```\nI like keeping these two decisions explicit:\n\n- \n`optionalKey(...)` → the property may be absent\n- \n`nullable(...)` → the value may be`null`\n\nThey look similar at first, but they describe different things.\n\n## 🌳 Nested Types Don't Need to Be Duplicated Either\n\nNow imagine a larger type:\n\n\n```\ntype Account = {\n  readonly id: string;\n  readonly profile: {\n    readonly displayName: string;\n    readonly bio: string | null;\n  } | null;\n  readonly tags: readonly string[];\n};\n```\nWe could manually copy the profile shape into another type.\n\nBut that creates another thing that can drift.\n\nInstead, we can reference the type we already have\n\n\n```\nimport { arrayOf, isString, nullable, typedStruct } from \"is-kit\";\nconst isProfile = typedStruct<NonNullable<Account[\"profile\"]>>()({\n  displayName: isString,\n  bio: nullable(isString),\n});\nconst isAccount = typedStruct<Account>()({\n  id: isString,\n  profile: nullable(isProfile),\n  tags: arrayOf(isString),\n});\n```\nThis is the model I like:\n\nReuse the existing type at compile time. Compose small guards at runtime.\n\n\nThe application type remains the source we want the guard to follow.\n\n## 🔒 What About Extra Runtime Properties?\n\nThere is another distinction worth making.\n\nThese are two different questions:\n\n1. Does my **guard definition** match the TypeScript type?\n2. Should a **runtime object** be allowed to contain additional properties?\n\nBy default, an object can still have additional keys.\n\nIf you want the runtime object shape to be closed as well, you can enable exact mode:\n\n\n```\nimport { isString, typedStruct } from \"is-kit\";\ntype User = {\n  id: string;\n  name: string;\n};\nconst isExactUser = typedStruct<User>()(\n  {\n    id: isString,\n    name: isString,\n  },\n  {\n    exact: true,\n  },\n);\n```\nThen:\n\n\n```\nisExactUser({\n  id: \"user-1\",\n  name: \"Ada\",\n});\n// true\nisExactUser({\n  id: \"user-1\",\n  name: \"Ada\",\n  debug: true,\n});\n// false\n```\nWhether extra properties should be rejected is a runtime policy decision.\n\nIt shouldn't be confused with keeping the guard definition synchronized with the TypeScript type.\n\n## ⚖️ Which Should Be the Source of Truth?\n\nI don't think there is one correct validation style for every project.\n\nThe important question is\n\nWhat already owns the shape of this data?\n\n\n### Manual predicate\n\n```\nconst isSomething = (value: unknown): value is Something => {\n  // custom logic\n};\n```\nGreat when the validation is unusual or not primarily structural.\n\n### Guard-first\n\n```\nconst isUser = struct({\n  id: isString,\n  name: isString,\n});\n```\nUseful when the guard itself should define the resulting type.\n\n### Type-first\n\n```\nconst isUser = typedStruct<User>()({\n  id: isString,\n  name: isString,\n});\n```\nUseful when `User` already exists and the runtime guard needs to stay aligned with it.\n\n### Schema-first\n\nA schema library or code generation may be the better source of truth when you need things like:\n\n- structured validation errors\n- coercion\n- transforms\n- defaults\n- generated artifacts\n\nThese solve different problems.\n\nI don't think every boolean validation check needs to become a schema. 😸\n\n\n## \n  \n  \n  🚫 What `typedStruct` Does Not Do\n\nThere are some important boundaries.\n\n`typedStruct` does **not** generate runtime validation from a TypeScript type.\n\nTypes are erased at runtime, so you still need to declare the guards you want to execute.\n\nIt also doesn't:\n\n- prove that every custom predicate is honest\n- coerce values\n- return rich structured validation errors\n- replace schema-first workflows\n- validate numeric or symbol properties as part of its string-keyed object contract\n\nIt's intentionally smaller than that.\n\nThe goal is simply to create a typed bridge between\n\nthe object type you already have\n\n\nand\n\nthe runtime guards you choose to run\n\n\n## 🎯 The Important Part\n\nThe main point isn't really `typedStruct`.\n\nIt's this\n\n**A type predicate is a promise, not a proof.**\n\n\nThis\n\n\n```\n(value): value is User\n```\ndoesn't mean TypeScript inspected your implementation and proved that every `User` field was validated.\n\nWe made that promise.\n\nSo when a TypeScript type is the source of truth, I think it's useful to make the runtime guard structurally depend on that type instead of relying on us to remember every future change.\n\nThat's what I wanted `typedStruct` to help with. 😸\n\nIf your guard defines the type, use a guard-first approach.\n\nIf an existing TypeScript type should define the contract, connect the guard to that type.\n\nAnd if you need rich parsing, transforms, coercion, or detailed errors, that's where a schema starts to earn its weight.\n\nI wrote a more complete guide about this on the is-kit documentation site:\n\nIf you like small reusable TypeScript type guards, `is-kit` is open source too!\n\n## nyaomaru / is-kit\n\n### Build small guards. Compose them. Lightweight, zero-dependency TypeScript type guards for runtime validation and natural narrowing. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨.\n\n# is-kit\n\n## Build small guards. Compose them.\n\n`is-kit` is a lightweight, zero-dependency toolkit for building reusable TypeScript **type guards**.\n\nIt helps you write small `isFoo` functions, compose them into **richer runtime checks**, and keep **TypeScript narrowing** natural inside regular control flow.\n\n**Runtime-safe** 🛡️, **composable** 🧩, and **ergonomic** ✨ without asking you to adopt a heavy schema workflow.\n\n- Build and reuse **typed guards**\n- \n**Compose guards** with`and` ,`or` ,`not` ,`oneOf`\n- \n**Validate object** shapes and collections\n- \n**Parse or assert**`unknown` values without a large schema framework\n\n📚 Documentation Site · 🧭 Practical Guides\n\nBest for **app-internal narrowing, filtering, and reusable guards**.\n\n\n## 🤔 Why use `is-kit`?\n\nTired of rewriting the same `isFoo` checks again and again?\n\n`is-kit` is a good fit when you want to:\n\n- \n**write reusable `isX`** functions instead of one-off inline checks\n- keep runtime validation **lightweight and dependency-free**\n- \n**narrow values directly** in`if` ,`filter` …\n\nThanks for reading! 🙌","body_html":"<h1 id=\"your-type-guard-can-silently-drift-from-your-typescript-type\">Your Type Guard Can Silently Drift from Your TypeScript Type</h1>\n<p>Hoi hoi! 👋</p>\n<p>I&#39;m @nyaomaru, a frontend engineer just back from a short vacation on Texel, a small island in the Netherlands. 😸🏝️</p>\n<p>Today, let&#39;s talk about a type guard that looks completely safe.</p>\n<pre><code>const isUser = (value: unknown): value is User =&gt; {\n  // runtime checks...\n};</code></pre>\n<p>Looks good, right?</p>\n<p>TypeScript knows that when <code>isUser(value)</code> returns <code>true</code>, the value is a <code>User</code>.</p>\n<p>But there&#39;s a small problem</p>\n<p>TypeScript trusts that promise.</p>\n<p>It doesn&#39;t prove that your runtime checks actually validate every field in <code>User</code>.</p>\n<p>And that&#39;s where type guards can slowly drift away from the types they claim to protect.</p>\n<p>Let&#39;s take a look! 👀</p>\n<h2 id=\"a-type-guard-can-become-outdated-without-an-error\">🕳️ A Type Guard Can Become Outdated Without an Error</h2>\n<p>Imagine we start with this type</p>\n<pre><code>type User = {\n  id: string;\n  name: string;\n};</code></pre>\n<p>And a hand-written type guard</p>\n<pre><code>const isUser = (value: unknown): value is User =&gt; {\n  if (typeof value !== &quot;object&quot; || value === null) {\n    return false;\n  }\n  const candidate = value as Record&lt;string, unknown&gt;;\n  return typeof candidate.id === &quot;string&quot; &amp;&amp; typeof candidate.name === &quot;string&quot;;\n};</code></pre>\n<p>So far, everything matches.</p>\n<p>Later, we update <code>User</code></p>\n<pre><code>type User = {\n  id: string;\n  name: string;\n  role: &quot;admin&quot; | &quot;member&quot;;\n};</code></pre>\n<p>But we forget to update the guard.</p>\n<pre><code>const isUser = (value: unknown): value is User =&gt; {\n  if (typeof value !== &quot;object&quot; || value === null) {\n    return false;\n  }\n  const candidate = value as Record&lt;string, unknown&gt;;\n  return typeof candidate.id === &quot;string&quot; &amp;&amp; typeof candidate.name === &quot;string&quot;;\n};</code></pre>\n<p>There is no <code>role</code> check.</p>\n<p>But this still compiles. 😿</p>\n<h2 id=\"why-doesn-t-typescript-catch-this\">🧠 Why Doesn&#39;t TypeScript Catch This?</h2>\n<p>Because this</p>\n<pre><code>(value: unknown): value is User</code></pre>\n<p>is a user-defined type predicate.</p>\n<p>You are telling TypeScript</p>\n<p>Trust me. If this function returns <code>true</code>, the value is a <code>User</code>.</p>\n<p>TypeScript can check whether the declared predicate type itself makes sense.</p>\n<p>But it cannot generally prove that arbitrary runtime logic actually validates every part of that type.</p>\n<p>So this is possible</p>\n<pre><code>const isUser = (_value: unknown): _value is User =&gt; true;</code></pre>\n<p>Terrible guard.</p>\n<p>Perfectly valid TypeScript. 😹</p>\n<p>The return type is a contract written by us, not a proof generated from the function body.</p>\n<h2 id=\"this-becomes-a-maintenance-problem\">🔄 This Becomes a Maintenance Problem</h2>\n<p>The annoying part isn&#39;t writing the guard once.</p>\n<p>It&#39;s keeping these two things synchronized over time</p>\n<pre><code>TypeScript type\n      ↕\nRuntime validation</code></pre>\n<p>Types change.</p>\n<p>Properties get</p>\n<ul><li>added</li><li>removed</li><li>renamed</li><li>made optional</li><li>changed to another type</li></ul>\n<p>And every time that happens, we need to remember that some runtime guard somewhere may also need an update.</p>\n<p>If we forget, the compiler may not tell us.</p>\n<p>That&#39;s the kind of bug I really don&#39;t want to rely on memory to prevent.</p>\n<h2 id=\"what-if-the-type-could-be-the-contract\">✅ What If the Type Could Be the Contract?</h2>\n<p>This is one of the reasons I added <code>typedStruct</code> to <code>is-kit</code>.</p>\n<p>Suppose the application type already exists:</p>\n<pre><code>type User = {\n  id: string;\n  name: string;\n  age?: number;\n};</code></pre>\n<p>We can build the guard against that existing type</p>\n<pre><code>import { isNumber, isString, optionalKey, typedStruct } from &quot;is-kit&quot;;\nconst isUser = typedStruct&lt;User&gt;()({\n  id: isString,\n  name: isString,\n  age: optionalKey(isNumber),\n});</code></pre>\n<p>Now the field map has a type-level relationship with <code>User</code>.</p>\n<p>At runtime, it still performs ordinary object validation.</p>\n<p>But at compile time, TypeScript can check whether the guards we declared match the object type they&#39;re supposed to follow.</p>\n<h2 id=\"now-drift-becomes-visible\">💥 Now Drift Becomes Visible</h2>\n<p>Let&#39;s add a field again</p>\n<pre><code>type User = {\n  id: string;\n  name: string;\n  role: &quot;admin&quot; | &quot;member&quot;;\n  age?: number;\n};</code></pre>\n<p>But forget to update the guard</p>\n<pre><code>typedStruct&lt;User&gt;()({\n  id: isString,\n  name: isString,\n  age: optionalKey(isNumber),\n  // TypeScript error:\n  // role is missing\n});</code></pre>\n<p>Nice.</p>\n<p>The runtime bug became a compile-time problem.</p>\n<p>The same thing happens if the guard uses an incompatible field type</p>\n<pre><code>import {\n  isNumber,\n  isString,\n  oneOfValues,\n  optionalKey,\n  typedStruct,\n} from &quot;is-kit&quot;;\ntypedStruct&lt;User&gt;()({\n  id: isString,\n  name: isNumber,\n  // TypeScript error:\n  // User[&quot;name&quot;] is string\n  role: oneOfValues(&quot;admin&quot;, &quot;member&quot;),\n  age: optionalKey(isNumber),\n});</code></pre>\n<p>This is the part I care about most.</p>\n<p><code>typedStruct</code> doesn&#39;t eliminate maintenance.</p>\n<p>It makes forgotten maintenance visible.</p>\n<h2 id=\"optional-and-nullable-are-different\">🧩 Optional and Nullable Are Different</h2>\n<p>Another place where object guards can get confusing is optional properties.</p>\n<p>Consider:</p>\n<pre><code>type User = {\n  id: string;\n  nickname?: string | null;\n};</code></pre>\n<p>There are two separate ideas here</p>\n<pre><code>nickname may be absent</code></pre>\n<p>and</p>\n<pre><code>nickname may exist with the value null</code></pre>\n<p>Those are different runtime contracts.</p>\n<p>With <code>typedStruct</code></p>\n<pre><code>import { isString, nullable, optionalKey, typedStruct } from &quot;is-kit&quot;;\nconst isUser = typedStruct&lt;User&gt;()({\n  id: isString,\n  nickname: optionalKey(nullable(isString)),\n});</code></pre>\n<p>Now</p>\n<pre><code>isUser({ id: &quot;user-1&quot; });\n// true\nisUser({\n  id: &quot;user-1&quot;,\n  nickname: null,\n});\n// true\nisUser({\n  id: &quot;user-1&quot;,\n  nickname: &quot;Neko&quot;,\n});\n// true\nisUser({\n  id: &quot;user-1&quot;,\n  nickname: 42,\n});\n// false</code></pre>\n<p>I like keeping these two decisions explicit:</p>\n<ul><li></li></ul>\n<p><code>optionalKey(...)</code> → the property may be absent</p>\n<ul><li></li></ul>\n<p><code>nullable(...)</code> → the value may be<code>null</code></p>\n<p>They look similar at first, but they describe different things.</p>\n<h2 id=\"nested-types-don-t-need-to-be-duplicated-either\">🌳 Nested Types Don&#39;t Need to Be Duplicated Either</h2>\n<p>Now imagine a larger type:</p>\n<pre><code>type Account = {\n  readonly id: string;\n  readonly profile: {\n    readonly displayName: string;\n    readonly bio: string | null;\n  } | null;\n  readonly tags: readonly string[];\n};</code></pre>\n<p>We could manually copy the profile shape into another type.</p>\n<p>But that creates another thing that can drift.</p>\n<p>Instead, we can reference the type we already have</p>\n<pre><code>import { arrayOf, isString, nullable, typedStruct } from &quot;is-kit&quot;;\nconst isProfile = typedStruct&lt;NonNullable&lt;Account[&quot;profile&quot;]&gt;&gt;()({\n  displayName: isString,\n  bio: nullable(isString),\n});\nconst isAccount = typedStruct&lt;Account&gt;()({\n  id: isString,\n  profile: nullable(isProfile),\n  tags: arrayOf(isString),\n});</code></pre>\n<p>This is the model I like:</p>\n<p>Reuse the existing type at compile time. Compose small guards at runtime.</p>\n<p>The application type remains the source we want the guard to follow.</p>\n<h2 id=\"what-about-extra-runtime-properties\">🔒 What About Extra Runtime Properties?</h2>\n<p>There is another distinction worth making.</p>\n<p>These are two different questions:</p>\n<ol><li>Does my <strong>guard definition</strong> match the TypeScript type?</li><li>Should a <strong>runtime object</strong> be allowed to contain additional properties?</li></ol>\n<p>By default, an object can still have additional keys.</p>\n<p>If you want the runtime object shape to be closed as well, you can enable exact mode:</p>\n<pre><code>import { isString, typedStruct } from &quot;is-kit&quot;;\ntype User = {\n  id: string;\n  name: string;\n};\nconst isExactUser = typedStruct&lt;User&gt;()(\n  {\n    id: isString,\n    name: isString,\n  },\n  {\n    exact: true,\n  },\n);</code></pre>\n<p>Then:</p>\n<pre><code>isExactUser({\n  id: &quot;user-1&quot;,\n  name: &quot;Ada&quot;,\n});\n// true\nisExactUser({\n  id: &quot;user-1&quot;,\n  name: &quot;Ada&quot;,\n  debug: true,\n});\n// false</code></pre>\n<p>Whether extra properties should be rejected is a runtime policy decision.</p>\n<p>It shouldn&#39;t be confused with keeping the guard definition synchronized with the TypeScript type.</p>\n<h2 id=\"which-should-be-the-source-of-truth\">⚖️ Which Should Be the Source of Truth?</h2>\n<p>I don&#39;t think there is one correct validation style for every project.</p>\n<p>The important question is</p>\n<p>What already owns the shape of this data?</p>\n<h3 id=\"manual-predicate\">Manual predicate</h3>\n<pre><code>const isSomething = (value: unknown): value is Something =&gt; {\n  // custom logic\n};</code></pre>\n<p>Great when the validation is unusual or not primarily structural.</p>\n<h3 id=\"guard-first\">Guard-first</h3>\n<pre><code>const isUser = struct({\n  id: isString,\n  name: isString,\n});</code></pre>\n<p>Useful when the guard itself should define the resulting type.</p>\n<h3 id=\"type-first\">Type-first</h3>\n<pre><code>const isUser = typedStruct&lt;User&gt;()({\n  id: isString,\n  name: isString,\n});</code></pre>\n<p>Useful when <code>User</code> already exists and the runtime guard needs to stay aligned with it.</p>\n<h3 id=\"schema-first\">Schema-first</h3>\n<p>A schema library or code generation may be the better source of truth when you need things like:</p>\n<ul><li>structured validation errors</li><li>coercion</li><li>transforms</li><li>defaults</li><li>generated artifacts</li></ul>\n<p>These solve different problems.</p>\n<p>I don&#39;t think every boolean validation check needs to become a schema. 😸</p>\n<p>## </p>\n<p>  🚫 What <code>typedStruct</code> Does Not Do</p>\n<p>There are some important boundaries.</p>\n<p><code>typedStruct</code> does <strong>not</strong> generate runtime validation from a TypeScript type.</p>\n<p>Types are erased at runtime, so you still need to declare the guards you want to execute.</p>\n<p>It also doesn&#39;t:</p>\n<ul><li>prove that every custom predicate is honest</li><li>coerce values</li><li>return rich structured validation errors</li><li>replace schema-first workflows</li><li>validate numeric or symbol properties as part of its string-keyed object contract</li></ul>\n<p>It&#39;s intentionally smaller than that.</p>\n<p>The goal is simply to create a typed bridge between</p>\n<p>the object type you already have</p>\n<p>and</p>\n<p>the runtime guards you choose to run</p>\n<h2 id=\"the-important-part\">🎯 The Important Part</h2>\n<p>The main point isn&#39;t really <code>typedStruct</code>.</p>\n<p>It&#39;s this</p>\n<p><strong>A type predicate is a promise, not a proof.</strong></p>\n<p>This</p>\n<pre><code>(value): value is User</code></pre>\n<p>doesn&#39;t mean TypeScript inspected your implementation and proved that every <code>User</code> field was validated.</p>\n<p>We made that promise.</p>\n<p>So when a TypeScript type is the source of truth, I think it&#39;s useful to make the runtime guard structurally depend on that type instead of relying on us to remember every future change.</p>\n<p>That&#39;s what I wanted <code>typedStruct</code> to help with. 😸</p>\n<p>If your guard defines the type, use a guard-first approach.</p>\n<p>If an existing TypeScript type should define the contract, connect the guard to that type.</p>\n<p>And if you need rich parsing, transforms, coercion, or detailed errors, that&#39;s where a schema starts to earn its weight.</p>\n<p>I wrote a more complete guide about this on the is-kit documentation site:</p>\n<p>If you like small reusable TypeScript type guards, <code>is-kit</code> is open source too!</p>\n<h2 id=\"nyaomaru-is-kit\">nyaomaru / is-kit</h2>\n<h3 id=\"build-small-guards-compose-them-lightweight-zero-dependency-type\">Build small guards. Compose them. Lightweight, zero-dependency TypeScript type guards for runtime validation and natural narrowing. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨.</h3>\n<h1 id=\"is-kit\">is-kit</h1>\n<h2 id=\"build-small-guards-compose-them\">Build small guards. Compose them.</h2>\n<p><code>is-kit</code> is a lightweight, zero-dependency toolkit for building reusable TypeScript <strong>type guards</strong>.</p>\n<p>It helps you write small <code>isFoo</code> functions, compose them into <strong>richer runtime checks</strong>, and keep <strong>TypeScript narrowing</strong> natural inside regular control flow.</p>\n<p><strong>Runtime-safe</strong> 🛡️, <strong>composable</strong> 🧩, and <strong>ergonomic</strong> ✨ without asking you to adopt a heavy schema workflow.</p>\n<ul><li>Build and reuse <strong>typed guards</strong></li><li></li></ul>\n<p><strong>Compose guards</strong> with<code>and</code> ,<code>or</code> ,<code>not</code> ,<code>oneOf</code></p>\n<ul><li></li></ul>\n<p><strong>Validate object</strong> shapes and collections</p>\n<ul><li></li></ul>\n<p><strong>Parse or assert</strong><code>unknown</code> values without a large schema framework</p>\n<p>📚 Documentation Site · 🧭 Practical Guides</p>\n<p>Best for <strong>app-internal narrowing, filtering, and reusable guards</strong>.</p>\n<h2 id=\"why-use-is-kit\">🤔 Why use <code>is-kit</code>?</h2>\n<p>Tired of rewriting the same <code>isFoo</code> checks again and again?</p>\n<p><code>is-kit</code> is a good fit when you want to:</p>\n<ul><li></li></ul>\n<p><strong>write reusable <code>isX</code></strong> functions instead of one-off inline checks</p>\n<ul><li>keep runtime validation <strong>lightweight and dependency-free</strong></li><li></li></ul>\n<p><strong>narrow values directly</strong> in<code>if</code> ,<code>filter</code> …</p>\n<p>Thanks for reading! 🙌</p>","headings":[{"level":1,"text":"Your Type Guard Can Silently Drift from Your TypeScript Type","id":"your-type-guard-can-silently-drift-from-your-typescript-type"},{"level":2,"text":"🕳️ A Type Guard Can Become Outdated Without an Error","id":"a-type-guard-can-become-outdated-without-an-error"},{"level":2,"text":"🧠 Why Doesn't TypeScript Catch This?","id":"why-doesn-t-typescript-catch-this"},{"level":2,"text":"🔄 This Becomes a Maintenance Problem","id":"this-becomes-a-maintenance-problem"},{"level":2,"text":"✅ What If the Type Could Be the Contract?","id":"what-if-the-type-could-be-the-contract"},{"level":2,"text":"💥 Now Drift Becomes Visible","id":"now-drift-becomes-visible"},{"level":2,"text":"🧩 Optional and Nullable Are Different","id":"optional-and-nullable-are-different"},{"level":2,"text":"🌳 Nested Types Don't Need to Be Duplicated Either","id":"nested-types-don-t-need-to-be-duplicated-either"},{"level":2,"text":"🔒 What About Extra Runtime Properties?","id":"what-about-extra-runtime-properties"},{"level":2,"text":"⚖️ Which Should Be the Source of Truth?","id":"which-should-be-the-source-of-truth"},{"level":3,"text":"Manual predicate","id":"manual-predicate"},{"level":3,"text":"Guard-first","id":"guard-first"},{"level":3,"text":"Type-first","id":"type-first"},{"level":3,"text":"Schema-first","id":"schema-first"},{"level":2,"text":"🎯 The Important Part","id":"the-important-part"},{"level":2,"text":"nyaomaru / is-kit","id":"nyaomaru-is-kit"},{"level":3,"text":"Build small guards. Compose them. Lightweight, zero-dependency TypeScript type guards for runtime validation and natural narrowing. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨.","id":"build-small-guards-compose-them-lightweight-zero-dependency-type"},{"level":1,"text":"is-kit","id":"is-kit"},{"level":2,"text":"Build small guards. Compose them.","id":"build-small-guards-compose-them"},{"level":2,"text":"🤔 Why use is-kit?","id":"why-use-is-kit"}]}}