[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-typescript-as-const-assertions":92,"path-membership-typescript-as-const-assertions":309,"related-typescript-as-const-assertions":310,"code:ts:true:78vkuv":441,"code:ts:true:19mbpzb":442,"code:ts:true:wsyeuk":443,"code:ts:true:1lfi3x":444,"code:ts:true:1sntx7f":445,"code:ts:true:x6t3jv":446,"code:ts:true:ewy9ml":447,"code:ts:true:149120h":448,"code:ts:true:u4l4rg":449,"code:ts:true:192qtl7":450,"comments-01a157ee-b1c1-7777-8aa6-ef7aa4ef6bd3":451},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,[45,49,53,57,61,65,69,73,77,81,85,88],{"slug":46,"name":47,"articles":48},"webdev","Webdev",152,{"slug":50,"name":51,"articles":52},"javascript","Javascript",126,{"slug":54,"name":55,"articles":56},"frontend","Frontend",88,{"slug":58,"name":59,"articles":60},"tutorial","Tutorial",59,{"slug":62,"name":63,"articles":64},"css","Css",45,{"slug":66,"name":67,"articles":68},"performance","Performance",25,{"slug":70,"name":71,"articles":72},"typescript","Typescript",22,{"slug":74,"name":75,"articles":76},"react","React",19,{"slug":78,"name":79,"articles":80},"browser","Browser",14,{"slug":82,"name":83,"articles":84},"programming","Programming",13,{"slug":86,"name":87,"articles":84},"node","Node",{"slug":89,"name":90,"articles":91},"accessibility","Accessibility",10,{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":101,"likeCount":19,"commentCount":19,"author":102,"vertical":107,"topic":108,"tags":110,"_count":115,"playground":117,"body":119,"bodyMd":292,"seo":293,"translationGroupId":296,"series":96,"podcastUrl":96,"verticalId":5,"thread":297,"assessments":299,"translations":305,"quiz":307},"01a157ee-b1c1-7777-8aa6-ef7aa4ef6bd3","typescript-as-const-assertions","My Reducer's Switch Stopped Being Exhaustive. `as const` Fixed It.",null,"Object and array literals widen to `string` and `number` the moment you write them, which quietly breaks discriminated unions and exhaustive switch checks. `as const` keeps the literal types — but it also freezes the value, which bites in its own way.","\u002Fmedia\u002Fcovers\u002Ftypescript-as-const-assertions.png",7,"2026-10-25T19:53:06.537Z",27,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":109},"TypeScript",[111,112,113,114],{"slug":70,"name":71,"color":96},{"slug":50,"name":51,"color":96},{"slug":46,"name":47,"color":96},{"slug":82,"name":83,"color":96},{"assessments":116},1,{"slug":94,"title":118},"as const — catch the missing case, not the bug report",{"blocks":120,"version":116},[121,125,128,134,137,140,144,147,151,154,158,161,165,168,171,175,178,182,185,189,192,196,199,202,205,208,211,214,217,220,224,227,231,234,238,241,245,253,256,259,262,265,268,271,274,277,280,283,286],{"id":122,"html":123,"type":124},"b1","\u003Cp>The PR review catches it at the worst possible moment — the day after it shipped. A teammate added a \u003Ccode>&#39;reset&#39;\u003C\u002Fcode> action to the cart reducer, ran the tests, watched them pass, and merged. The switch statement never ran the new branch. Not an error. Not a warning. The build was green the whole time.\u003C\u002Fp>","paragraph",{"id":126,"html":127,"type":124},"b2","\u003Cp>Here&#39;s the reducer, trimmed down to the shape that matters:\u003C\u002Fp>",{"id":129,"code":130,"type":131,"language":132,"highlight":133},"b3","function cartReducer(state: CartState, action: { type: string; payload?: unknown }) {\n  switch (action.type) {\n    case 'add':\n      return { ...state, items: [...state.items, action.payload] };\n    case 'remove':\n      return { ...state, items: state.items.filter((i) => i.id !== action.payload) };\n    \u002F\u002F 'reset' was added here as an afterthought, then deleted by accident\n    \u002F\u002F during a rebase. TypeScript never noticed either way.\n  }\n  return state;\n}","code","ts",[],{"id":135,"html":136,"type":124},"b4","\u003Cp>\u003Ccode>action.type\u003C\u002Fcode> is typed as \u003Ccode>string\u003C\u002Fcode>. TypeScript can check a switch against a union of specific values and tell you when a case is missing. It cannot check a switch against \u003Cem>all possible strings\u003C\u002Fem> — there&#39;s no way to be &quot;exhaustive&quot; over an infinite type. So the compiler does the only honest thing: it says nothing.\u003C\u002Fp>",{"id":138,"html":139,"type":124},"b5","\u003Cp>The bug wasn&#39;t the missing case. The bug was \u003Ccode>string\u003C\u002Fcode> — and it was there before anyone touched the reducer.\u003C\u002Fp>",{"id":141,"html":142,"text":142,"type":143,"level":31},"b6","Where the type actually came from","heading",{"id":145,"html":146,"type":124},"b7","\u003Cp>Nobody typed \u003Ccode>action.type: string\u003C\u002Fcode> on purpose. It came from the action creators:\u003C\u002Fp>",{"id":148,"code":149,"type":131,"language":132,"highlight":150},"b8","function reset() {\n  return { type: 'reset' };\n}\n\nreset(); \u002F\u002F return type: { type: string }",[],{"id":152,"html":153,"type":124},"b9","\u003Cp>That&#39;s the part that surprises people. \u003Ccode>reset\u003C\u002Fcode> is a \u003Ccode>const\u003C\u002Fcode>-worthy, never-reassigned function returning a single literal. You&#39;d expect \u003Ccode>{ type: &#39;reset&#39; }\u003C\u002Fcode> to stay exactly that specific. It doesn&#39;t — and the reason is really about mutability, not constants.\u003C\u002Fp>",{"id":155,"code":156,"type":131,"language":132,"highlight":157},"b10","const status = 'idle';\n\u002F\u002F type: 'idle' — status can never change, so TypeScript keeps the literal\n\nconst config = { status: 'idle' };\n\u002F\u002F config.status type: string — config.status CAN be reassigned\n\u002F\u002F (config.status = 'loading' compiles fine), so TypeScript widens\n\u002F\u002F the property to the general type that covers anything you might assign",[],{"id":159,"html":160,"type":124},"b11","\u003Cp>A \u003Ccode>const\u003C\u002Fcode> binding to a primitive can&#39;t be reassigned, so TypeScript has no reason to generalize — the literal type is the only thing it will ever hold. A property \u003Cem>inside\u003C\u002Fem> an object is a different story: \u003Ccode>config\u003C\u002Fcode> is \u003Ccode>const\u003C\u002Fcode>, but \u003Ccode>config.status\u003C\u002Fcode> isn&#39;t. TypeScript widens object and array members to their general type — \u003Ccode>string\u003C\u002Fcode>, \u003Ccode>number\u003C\u002Fcode>, \u003Ccode>boolean\u003C\u002Fcode> — because it&#39;s inferring a type that has to tolerate future writes, not just describe the value in front of it. Same rule for arrays:\u003C\u002Fp>",{"id":162,"code":163,"type":131,"language":132,"highlight":164},"b12","const methods = ['GET', 'POST'];\n\u002F\u002F type: string[] — any string could get pushed in later",[],{"id":166,"html":167,"type":124},"b13","\u003Cp>That widening is usually what you want. It&#39;s only a problem when the whole point of the value was to be one of a fixed, known set — an action&#39;s \u003Ccode>type\u003C\u002Fcode>, a theme name, an HTTP method you&#39;re about to switch on.\u003C\u002Fp>",{"id":169,"html":170,"type":124},"b14","\u003Cp>Before you scroll — what do you think turns \u003Ccode>string\u003C\u002Fcode> back into the literal \u003Ccode>&#39;idle&#39;\u003C\u002Fcode>?\u003C\u002Fp>",{"id":172,"html":173,"text":174,"type":143,"level":31},"b15","\u003Ccode>as const\u003C\u002Fcode> tells TypeScript to stop generalizing","as const tells TypeScript to stop generalizing",{"id":176,"html":177,"type":124},"b16","\u003Cp>\u003Ccode>as const\u003C\u002Fcode> is a type assertion that does the opposite of widening: it takes the literal, narrowest type of exactly what you wrote, and makes every property (recursively) \u003Ccode>readonly\u003C\u002Fcode> so there&#39;s no future write to protect against.\u003C\u002Fp>",{"id":179,"code":180,"type":131,"language":132,"highlight":181},"b17","const config = { status: 'idle' } as const;\n\u002F\u002F config.status type: 'idle'  (readonly)\n\nconst methods = ['GET', 'POST'] as const;\n\u002F\u002F type: readonly ['GET', 'POST']  — a fixed-length, literal tuple, not string[]",[],{"id":183,"html":184,"type":124},"b18","\u003Cp>Apply it to the action creators and the whole chain fixes itself:\u003C\u002Fp>",{"id":186,"code":187,"type":131,"language":132,"highlight":188},"b19","function reset() {\n  return { type: 'reset' } as const;\n}\nfunction add(payload: Item) {\n  return { type: 'add', payload } as const;\n}\nfunction remove(id: string) {\n  return { type: 'remove', payload: id } as const;\n}\n\ntype CartAction = ReturnType\u003Ctypeof reset> | ReturnType\u003Ctypeof add> | ReturnType\u003Ctypeof remove>;\n\u002F\u002F { type: 'reset' } | { type: 'add'; payload: Item } | { type: 'remove'; payload: string }",[],{"id":190,"html":191,"type":124},"b20","\u003Cp>\u003Ccode>action.type\u003C\u002Fcode> inside the reducer is now \u003Ccode>&#39;add&#39; | &#39;remove&#39; | &#39;reset&#39;\u003C\u002Fcode> instead of \u003Ccode>string\u003C\u002Fcode> — a real, finite union. Switch over it, and TypeScript can finally do the one thing it couldn&#39;t before:\u003C\u002Fp>",{"id":193,"code":194,"type":131,"language":132,"highlight":195},"b21","function cartReducer(state: CartState, action: CartAction): CartState {\n  switch (action.type) {\n    case 'add':\n      return { ...state, items: [...state.items, action.payload] };\n    case 'remove':\n      return { ...state, items: state.items.filter((i) => i.id !== action.payload) };\n    default:\n      return assertNever(action); \u002F\u002F ← now a compile error if a case is missing\n  }\n}\n\nfunction assertNever(x: never): never {\n  throw new Error(`Unhandled action: ${JSON.stringify(x)}`);\n}",[],{"id":197,"html":198,"type":124},"b22","\u003Cp>Delete the \u003Ccode>&#39;reset&#39;\u003C\u002Fcode> case and \u003Ccode>action\u003C\u002Fcode> reaching the \u003Ccode>default\u003C\u002Fcode> branch is no longer \u003Ccode>never\u003C\u002Fcode> — it&#39;s \u003Ccode>{ type: &#39;reset&#39; }\u003C\u002Fcode>, which doesn&#39;t satisfy \u003Ccode>assertNever\u003C\u002Fcode>&#39;s \u003Ccode>never\u003C\u002Fcode> parameter. TypeScript flags it at the call site, at compile time, before a reviewer has to catch it by reading carefully. That&#39;s the whole fix: one three-letter suffix, in the three functions that build the actions.\u003C\u002Fp>",{"id":200,"html":201,"type":124},"b23","\u003C!-- playground:start -->",{"id":203,"html":204,"text":204,"type":143,"level":31},"b24","🎮 Try it yourself",{"id":206,"html":207,"type":124},"b25","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftypescript-as-const-assertions\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":209,"html":210,"type":124},"b26","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":212,"html":213,"type":124},"b27","\u003C!-- playground:end -->",{"id":215,"html":216,"text":216,"type":143,"level":31},"b28","The part that bites back",{"id":218,"html":219,"type":124},"b29","\u003Cp>Here&#39;s where it gets uglier, and where most explanations of \u003Ccode>as const\u003C\u002Fcode> stop too early. \u003Ccode>as const\u003C\u002Fcode> didn&#39;t just lock in the literal type — it made the value \u003Ccode>readonly\u003C\u002Fcode>, all the way down. That&#39;s not a side effect you can opt out of; it&#39;s the mechanism.\u003C\u002Fp>",{"id":221,"code":222,"type":131,"language":132,"highlight":223},"b30","const METHODS = ['GET', 'POST'] as const;\n\nMETHODS.push('PUT');\n\u002F\u002F Error: Property 'push' does not exist on type 'readonly [\"GET\", \"POST\"]'",[],{"id":225,"html":226,"type":124},"b31","\u003Cp>A \u003Ccode>readonly\u003C\u002Fcode> tuple has no mutating methods at all — no \u003Ccode>push\u003C\u002Fcode>, \u003Ccode>pop\u003C\u002Fcode>, \u003Ccode>splice\u003C\u002Fcode>, or index assignment. That&#39;s usually fine; you reached for \u003Ccode>as const\u003C\u002Fcode> because you wanted a fixed set, not a growable list. But it surfaces somewhere you won&#39;t expect the first time:\u003C\u002Fp>",{"id":228,"code":229,"type":131,"language":132,"highlight":230},"b32","function logAllowedMethods(methods: string[]) {\n  console.log(methods.join(', '));\n}\n\nlogAllowedMethods(METHODS);\n\u002F\u002F Error: Argument of type 'readonly [\"GET\", \"POST\"]' is not assignable\n\u002F\u002F to parameter of type 'string[]'. The type 'readonly [\"GET\", \"POST\"]'\n\u002F\u002F is 'readonly' and cannot be assigned to the mutable type 'string[]'.",[],{"id":232,"html":233,"type":124},"b33","\u003Cp>\u003Ccode>logAllowedMethods\u003C\u002Fcode> never mutates its argument, but its signature \u003Cem>promises\u003C\u002Fem> TypeScript it&#39;s allowed to. TypeScript can&#39;t verify that promise against a value it knows is frozen, so it refuses the call outright — correctly, because somewhere else \u003Ccode>methods.push(...)\u003C\u002Fcode> really would be legal against that same signature. Two ways out, and they mean different things:\u003C\u002Fp>",{"id":235,"code":236,"type":131,"language":132,"highlight":237},"b34","function logAllowedMethods(methods: readonly string[]) { \u002F* ... *\u002F }\nlogAllowedMethods(METHODS); \u002F\u002F fine — the parameter now admits it won't mutate\n\n\u002F\u002F or, if the function genuinely can't change:\nlogAllowedMethods([...METHODS]); \u002F\u002F fine — a fresh, mutable copy",[],{"id":239,"html":240,"type":124},"b35","\u003Cp>Widen the parameter if the function is well-behaved and just reads. Copy the array if you don&#39;t control the function. Either is one line; the trap is not knowing which bug you&#39;re looking at when the red squiggle shows up two files away from the \u003Ccode>as const\u003C\u002Fcode> that caused it.\u003C\u002Fp>",{"id":242,"html":243,"text":244,"type":143,"level":31},"b36","Where it&#39;s worth reaching for","Where it's worth reaching for",{"id":246,"type":247,"items":248,"ordered":18},"b37","list",[249,250,251,252],"\u003Cstrong>Action\u002Fevent objects\u003C\u002Fstrong> — reducers, event buses, anywhere a \u003Ccode>type\u003C\u002Fcode> or \u003Ccode>kind\u003C\u002Fcode> field drives a switch. This is the highest-leverage use; it&#39;s the difference between a real exhaustiveness check and a decorative one.","\u003Cstrong>Fixed option lists\u003C\u002Fstrong> — \u003Ccode>const THEMES = [&#39;light&#39;, &#39;dark&#39;, &#39;system&#39;] as const;\u003C\u002Fcode> then \u003Ccode>type Theme = (typeof THEMES)[number];\u003C\u002Fcode> gives you the union \u003Ccode>&#39;light&#39; | &#39;dark&#39; | &#39;system&#39;\u003C\u002Fcode> for free, generated from the one array you&#39;ll actually iterate to build a \u003Ccode>&lt;select&gt;\u003C\u002Fcode>.","\u003Cstrong>Enum-free constant maps\u003C\u002Fstrong> — \u003Ccode>const Role = { ADMIN: &#39;admin&#39;, EDITOR: &#39;editor&#39; } as const;\u003C\u002Fcode> reads better than a TypeScript \u003Ccode>enum\u003C\u002Fcode> for most teams and gives you \u003Ccode>(typeof Role)[keyof typeof Role]\u003C\u002Fcode> as the type.","\u003Cstrong>Not\u003C\u002Fstrong> for values you&#39;re going to mutate. If you need to \u003Ccode>.push()\u003C\u002Fcode> into it later, \u003Ccode>as const\u003C\u002Fcode> is actively the wrong tool — reach for an explicit type annotation instead.",{"id":254,"html":255,"type":124},"b38","\u003Cp>And if you&#39;re already validating the shape against an interface, you may not need \u003Ccode>as const\u003C\u002Fcode> at all — \u003Ccode>satisfies\u003C\u002Fcode> checks a value against a type \u003Cem>and\u003C\u002Fem> keeps its inferred literals, in one step, without the \u003Ccode>readonly\u003C\u002Fcode>. The two solve overlapping problems from opposite directions: \u003Ccode>as const\u003C\u002Fcode> narrows by removing information you didn&#39;t ask TypeScript to track (mutability); \u003Ccode>satisfies\u003C\u002Fcode> narrows by not throwing away information you already had. Worth knowing both before reaching for either.\u003C\u002Fp>",{"id":257,"html":258,"type":124},"b39","\u003Cp>The missing \u003Ccode>&#39;reset&#39;\u003C\u002Fcode> case made it to production because \u003Ccode>string\u003C\u002Fcode> doesn&#39;t have gaps to find. \u003Ccode>as const\u003C\u002Fcode> is how you tell TypeScript the value in front of it is the only value it will ever be — so when you miss one, the compiler misses it too, loudly, before a reviewer has to.\u003C\u002Fp>",{"id":260,"html":261,"type":124},"b40","\u003Cp>Where has a widened type let a real bug through for you — and did you reach for \u003Ccode>as const\u003C\u002Fcode>, or something else?\u003C\u002Fp>",{"id":263,"html":264,"type":124},"b41","\u003C!-- quiz:start -->",{"id":266,"html":267,"text":267,"type":143,"level":31},"b42","🧠 Test yourself",{"id":269,"html":270,"type":124},"b43","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftypescript-as-const-assertions\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":272,"html":273,"type":124},"b44","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":275,"html":276,"type":124},"b45","\u003C!-- quiz:end -->",{"id":278,"type":279},"b46","divider",{"id":281,"html":282,"type":124},"b47","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":284,"html":285,"type":124},"b48","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":287,"type":247,"items":288,"ordered":18},"b49",[289,290,291],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","The PR review catches it at the worst possible moment — the day after it shipped. A teammate added a `'reset'` action to the cart reducer, ran the tests, watched them pass, and merged. The switch statement never ran the new branch. Not an error. Not a warning. The build was green the whole time.\n\nHere's the reducer, trimmed down to the shape that matters:\n\n```ts\nfunction cartReducer(state: CartState, action: { type: string; payload?: unknown }) {\n  switch (action.type) {\n    case 'add':\n      return { ...state, items: [...state.items, action.payload] };\n    case 'remove':\n      return { ...state, items: state.items.filter((i) => i.id !== action.payload) };\n    \u002F\u002F 'reset' was added here as an afterthought, then deleted by accident\n    \u002F\u002F during a rebase. TypeScript never noticed either way.\n  }\n  return state;\n}\n```\n\n`action.type` is typed as `string`. TypeScript can check a switch against a union of specific values and tell you when a case is missing. It cannot check a switch against *all possible strings* — there's no way to be \"exhaustive\" over an infinite type. So the compiler does the only honest thing: it says nothing.\n\nThe bug wasn't the missing case. The bug was `string` — and it was there before anyone touched the reducer.\n\n## Where the type actually came from\n\nNobody typed `action.type: string` on purpose. It came from the action creators:\n\n```ts\nfunction reset() {\n  return { type: 'reset' };\n}\n\nreset(); \u002F\u002F return type: { type: string }\n```\n\nThat's the part that surprises people. `reset` is a `const`-worthy, never-reassigned function returning a single literal. You'd expect `{ type: 'reset' }` to stay exactly that specific. It doesn't — and the reason is really about mutability, not constants.\n\n```ts\nconst status = 'idle';\n\u002F\u002F type: 'idle' — status can never change, so TypeScript keeps the literal\n\nconst config = { status: 'idle' };\n\u002F\u002F config.status type: string — config.status CAN be reassigned\n\u002F\u002F (config.status = 'loading' compiles fine), so TypeScript widens\n\u002F\u002F the property to the general type that covers anything you might assign\n```\n\nA `const` binding to a primitive can't be reassigned, so TypeScript has no reason to generalize — the literal type is the only thing it will ever hold. A property *inside* an object is a different story: `config` is `const`, but `config.status` isn't. TypeScript widens object and array members to their general type — `string`, `number`, `boolean` — because it's inferring a type that has to tolerate future writes, not just describe the value in front of it. Same rule for arrays:\n\n```ts\nconst methods = ['GET', 'POST'];\n\u002F\u002F type: string[] — any string could get pushed in later\n```\n\nThat widening is usually what you want. It's only a problem when the whole point of the value was to be one of a fixed, known set — an action's `type`, a theme name, an HTTP method you're about to switch on.\n\nBefore you scroll — what do you think turns `string` back into the literal `'idle'`?\n\n## `as const` tells TypeScript to stop generalizing\n\n`as const` is a type assertion that does the opposite of widening: it takes the literal, narrowest type of exactly what you wrote, and makes every property (recursively) `readonly` so there's no future write to protect against.\n\n```ts\nconst config = { status: 'idle' } as const;\n\u002F\u002F config.status type: 'idle'  (readonly)\n\nconst methods = ['GET', 'POST'] as const;\n\u002F\u002F type: readonly ['GET', 'POST']  — a fixed-length, literal tuple, not string[]\n```\n\nApply it to the action creators and the whole chain fixes itself:\n\n```ts\nfunction reset() {\n  return { type: 'reset' } as const;\n}\nfunction add(payload: Item) {\n  return { type: 'add', payload } as const;\n}\nfunction remove(id: string) {\n  return { type: 'remove', payload: id } as const;\n}\n\ntype CartAction = ReturnType\u003Ctypeof reset> | ReturnType\u003Ctypeof add> | ReturnType\u003Ctypeof remove>;\n\u002F\u002F { type: 'reset' } | { type: 'add'; payload: Item } | { type: 'remove'; payload: string }\n```\n\n`action.type` inside the reducer is now `'add' | 'remove' | 'reset'` instead of `string` — a real, finite union. Switch over it, and TypeScript can finally do the one thing it couldn't before:\n\n```ts\nfunction cartReducer(state: CartState, action: CartAction): CartState {\n  switch (action.type) {\n    case 'add':\n      return { ...state, items: [...state.items, action.payload] };\n    case 'remove':\n      return { ...state, items: state.items.filter((i) => i.id !== action.payload) };\n    default:\n      return assertNever(action); \u002F\u002F ← now a compile error if a case is missing\n  }\n}\n\nfunction assertNever(x: never): never {\n  throw new Error(`Unhandled action: ${JSON.stringify(x)}`);\n}\n```\n\nDelete the `'reset'` case and `action` reaching the `default` branch is no longer `never` — it's `{ type: 'reset' }`, which doesn't satisfy `assertNever`'s `never` parameter. TypeScript flags it at the call site, at compile time, before a reviewer has to catch it by reading carefully. That's the whole fix: one three-letter suffix, in the three functions that build the actions.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftypescript-as-const-assertions\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The part that bites back\n\nHere's where it gets uglier, and where most explanations of `as const` stop too early. `as const` didn't just lock in the literal type — it made the value `readonly`, all the way down. That's not a side effect you can opt out of; it's the mechanism.\n\n```ts\nconst METHODS = ['GET', 'POST'] as const;\n\nMETHODS.push('PUT');\n\u002F\u002F Error: Property 'push' does not exist on type 'readonly [\"GET\", \"POST\"]'\n```\n\nA `readonly` tuple has no mutating methods at all — no `push`, `pop`, `splice`, or index assignment. That's usually fine; you reached for `as const` because you wanted a fixed set, not a growable list. But it surfaces somewhere you won't expect the first time:\n\n```ts\nfunction logAllowedMethods(methods: string[]) {\n  console.log(methods.join(', '));\n}\n\nlogAllowedMethods(METHODS);\n\u002F\u002F Error: Argument of type 'readonly [\"GET\", \"POST\"]' is not assignable\n\u002F\u002F to parameter of type 'string[]'. The type 'readonly [\"GET\", \"POST\"]'\n\u002F\u002F is 'readonly' and cannot be assigned to the mutable type 'string[]'.\n```\n\n`logAllowedMethods` never mutates its argument, but its signature *promises* TypeScript it's allowed to. TypeScript can't verify that promise against a value it knows is frozen, so it refuses the call outright — correctly, because somewhere else `methods.push(...)` really would be legal against that same signature. Two ways out, and they mean different things:\n\n```ts\nfunction logAllowedMethods(methods: readonly string[]) { \u002F* ... *\u002F }\nlogAllowedMethods(METHODS); \u002F\u002F fine — the parameter now admits it won't mutate\n\n\u002F\u002F or, if the function genuinely can't change:\nlogAllowedMethods([...METHODS]); \u002F\u002F fine — a fresh, mutable copy\n```\n\nWiden the parameter if the function is well-behaved and just reads. Copy the array if you don't control the function. Either is one line; the trap is not knowing which bug you're looking at when the red squiggle shows up two files away from the `as const` that caused it.\n\n## Where it's worth reaching for\n\n- **Action\u002Fevent objects** — reducers, event buses, anywhere a `type` or `kind` field drives a switch. This is the highest-leverage use; it's the difference between a real exhaustiveness check and a decorative one.\n- **Fixed option lists** — `const THEMES = ['light', 'dark', 'system'] as const;` then `type Theme = (typeof THEMES)[number];` gives you the union `'light' | 'dark' | 'system'` for free, generated from the one array you'll actually iterate to build a `\u003Cselect>`.\n- **Enum-free constant maps** — `const Role = { ADMIN: 'admin', EDITOR: 'editor' } as const;` reads better than a TypeScript `enum` for most teams and gives you `(typeof Role)[keyof typeof Role]` as the type.\n- **Not** for values you're going to mutate. If you need to `.push()` into it later, `as const` is actively the wrong tool — reach for an explicit type annotation instead.\n\nAnd if you're already validating the shape against an interface, you may not need `as const` at all — `satisfies` checks a value against a type *and* keeps its inferred literals, in one step, without the `readonly`. The two solve overlapping problems from opposite directions: `as const` narrows by removing information you didn't ask TypeScript to track (mutability); `satisfies` narrows by not throwing away information you already had. Worth knowing both before reaching for either.\n\nThe missing `'reset'` case made it to production because `string` doesn't have gaps to find. `as const` is how you tell TypeScript the value in front of it is the only value it will ever be — so when you miss one, the compiler misses it too, loudly, before a reviewer has to.\n\nWhere has a widened type let a real bug through for you — and did you reach for `as const`, or something else?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftypescript-as-const-assertions\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":95,"canonical":294,"description":295},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftypescript-as-const-assertions","Object and array literals widen to `string` and `number` the moment you write them, which quietly breaks discriminated unions and exhaustive switch checks. `as const` keeps the lit","01a157ee-b1c2-7318-af01-3b185764d4af",{"id":298,"locked":18},"01a157ee-b1fa-70ff-9945-1c84db204b99",[300],{"id":301,"slug":94,"title":302,"_count":303},"01a157ee-b214-7367-868b-3053022fe4b5","as const: literal types, readonly tuples, and exhaustive switches",{"questions":304},8,[306],{"locale":13,"slug":94},{"id":301,"slug":94,"title":302,"_count":308,"questionCount":304},{"questions":304},[],{"items":311,"meta":439},[312,324,344,364,382,400,421],{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":313,"likeCount":19,"commentCount":19,"author":314,"vertical":315,"topic":316,"tags":317,"_count":322,"playground":323,"hasQuiz":17,"hasPlayground":17},28,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":109},[318,319,320,321],{"slug":70,"name":71,"color":96},{"slug":50,"name":51,"color":96},{"slug":46,"name":47,"color":96},{"slug":82,"name":83,"color":96},{"assessments":116},{"slug":94},{"id":325,"slug":326,"title":327,"subtitle":96,"excerpt":328,"coverUrl":329,"locale":13,"readingMinutes":84,"publishedAt":330,"viewCount":331,"likeCount":19,"commentCount":19,"author":332,"vertical":333,"topic":334,"tags":337,"_count":342,"playground":343,"hasQuiz":17,"hasPlayground":17},"01a1358a-158e-70df-8178-64af7e9a5932","nestjs-weekly-lifecycle-hook-order","NestJS 12 Lifecycle Hooks: The Order You Can't Assume","NestJS 12 changed how lifecycle hooks are ordered. Learn what onModuleInit and onApplicationBootstrap really guarantee, plus a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnestjs-weekly-lifecycle-hook-order.png","2026-10-23T06:10:32.276Z",150,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":335,"name":336},"nestjs","Nestjs",[338,339,340,341],{"slug":335,"name":336,"color":96},{"slug":70,"name":71,"color":96},{"slug":86,"name":87,"color":96},{"slug":58,"name":59,"color":96},{"assessments":116},{"slug":326},{"id":345,"slug":346,"title":347,"subtitle":96,"excerpt":348,"coverUrl":349,"locale":13,"readingMinutes":350,"publishedAt":351,"viewCount":352,"likeCount":19,"commentCount":19,"author":353,"vertical":354,"topic":355,"tags":357,"_count":362,"playground":363,"hasQuiz":17,"hasPlayground":17},"01a13e2e-f495-74ab-b10e-7e8cebdfc365","intl-numberformat-currency-formatting","Stop Formatting Currency With Regex. `Intl.NumberFormat` Does It.","A hand-rolled formatMoney() with toFixed() and a comma regex looks fine in a US demo and quietly breaks the moment a real user, a negative balance, or a non-dollar locale shows up. Intl.NumberFormat has handled all of it, built into every modern browser, since before most teams' codebases existed.","\u002Fmedia\u002Fcovers\u002Fintl-numberformat-currency-formatting.png",5,"2026-10-20T06:10:28.425Z",185,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":50,"name":356},"JavaScript",[358,359,360,361],{"slug":50,"name":51,"color":96},{"slug":46,"name":47,"color":96},{"slug":70,"name":71,"color":96},{"slug":82,"name":83,"color":96},{"assessments":116},{"slug":346},{"id":365,"slug":366,"title":367,"subtitle":96,"excerpt":368,"coverUrl":369,"locale":13,"readingMinutes":350,"publishedAt":370,"viewCount":371,"likeCount":19,"commentCount":19,"author":372,"vertical":373,"topic":374,"tags":375,"_count":380,"playground":381,"hasQuiz":17,"hasPlayground":17},"01a12ebb-c68a-7460-98eb-bc3859d715a0","inferred-type-predicates-filter-boolean","Inferred Type Predicates Won't Save filter(Boolean). Here's What Will.","TypeScript 5.5 added inferred type predicates, and the internet decided that meant filter(Boolean) finally narrows. It doesn't. Here's exactly what the feature fixed, why the mix-up happened, and the one-line pattern that actually gets you type-safe filtering.","\u002Fmedia\u002Fcovers\u002Finferred-type-predicates-filter-boolean.png","2026-10-17T12:17:17.948Z",208,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":109},[376,377,378,379],{"slug":70,"name":71,"color":96},{"slug":50,"name":51,"color":96},{"slug":82,"name":83,"color":96},{"slug":46,"name":47,"color":96},{"assessments":116},{"slug":366},{"id":383,"slug":384,"title":385,"subtitle":96,"excerpt":386,"coverUrl":387,"locale":13,"readingMinutes":80,"publishedAt":388,"viewCount":389,"likeCount":19,"commentCount":19,"author":390,"vertical":391,"topic":392,"tags":393,"_count":398,"playground":399,"hasQuiz":17,"hasPlayground":17},"01a0f0ef-ccc8-73e8-9b53-e6ce4c7fe9ff","nestjs-weekly-standard-schema-validation","NestJS 12: Standard Schema Validation Without class-validator","NestJS 12 adds Standard Schema validation for @Body(), @Query(), and @Param() — validate with Zod or Valibot, no class-validator DTO. Cheat sheet included.","\u002Fmedia\u002Fcovers\u002Fnestjs-weekly-standard-schema-validation.png","2026-10-02T06:11:02.679Z",478,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":335,"name":336},[394,395,396,397],{"slug":335,"name":336,"color":96},{"slug":86,"name":87,"color":96},{"slug":70,"name":71,"color":96},{"slug":58,"name":59,"color":96},{"assessments":116},{"slug":384},{"id":401,"slug":402,"title":403,"subtitle":96,"excerpt":404,"coverUrl":405,"locale":13,"readingMinutes":406,"publishedAt":407,"viewCount":408,"likeCount":19,"commentCount":19,"author":409,"vertical":410,"topic":411,"tags":414,"_count":419,"playground":420,"hasQuiz":17,"hasPlayground":17},"01a0ec73-0927-7298-a1ae-88b0d2458ab0","nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16","Next.js 15 made GET Route Handlers dynamic by default. What that changes for code written for 14, how to cache one on purpose in 16, plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-route-handlers-caching-streaming.png",15,"2026-09-29T09:16:08.176Z",673,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":412,"name":413},"nextjs","Nextjs",[415,416,417,418],{"slug":412,"name":413,"color":96},{"slug":70,"name":71,"color":96},{"slug":46,"name":47,"color":96},{"slug":58,"name":59,"color":96},{"assessments":116},{"slug":402},{"id":422,"slug":423,"title":424,"subtitle":96,"excerpt":425,"coverUrl":426,"locale":13,"readingMinutes":80,"publishedAt":427,"viewCount":428,"likeCount":19,"commentCount":19,"author":429,"vertical":430,"topic":431,"tags":432,"_count":437,"playground":438,"hasQuiz":17,"hasPlayground":17},"01a0ddae-48ac-73c6-bdee-1420cdfb17c2","nestjs-weekly-guards-canactivate-reflector","NestJS Guards: CanActivate, ExecutionContext & Reflector","A complete guide to NestJS guards: how CanActivate, ExecutionContext, and the Reflector decide who reaches your route handler, with a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnestjs-weekly-guards-canactivate-reflector.png","2026-09-26T12:25:39.001Z",611,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":335,"name":336},[433,434,435,436],{"slug":335,"name":336,"color":96},{"slug":86,"name":87,"color":96},{"slug":70,"name":71,"color":96},{"slug":58,"name":59,"color":96},{"assessments":116},{"slug":423},{"page":116,"perPage":99,"total":72,"totalPages":440},4,"\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">cartReducer\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CartState\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>; payload\u003Cspan class=\"shj-oper\">?:\u003C\u002Fspan> unknown \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">switch\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">case\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'add'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> items\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>items\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>payload\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">case\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'remove'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> items\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>items\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">filter\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>i\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> i\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>payload\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-cmnt\">\u002F\u002F 'reset' was added here as an afterthought, then deleted by accident\n\u003C\u002Fspan>    \u003Cspan class=\"shj-cmnt\">\u002F\u002F during a rebase. TypeScript never noticed either way.\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> state;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">reset\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'reset'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-func\">reset\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F return type: { type: string }\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> status \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'idle'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F type: 'idle' — status can never change, so TypeScript keeps the literal\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> config \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> status\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'idle'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F config.status type: string — config.status CAN be reassigned\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F (config.status = 'loading' compiles fine), so TypeScript widens\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F the property to the general type that covers anything you might assign\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> methods \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'GET'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'POST'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F type: string[] — any string could get pushed in later\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> config \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> status\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'idle'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F config.status type: 'idle'  (readonly)\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> methods \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'GET'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'POST'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F type: readonly ['GET', 'POST']  — a fixed-length, literal tuple, not string[]\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">reset\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'reset'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">add\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>payload\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Item\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'add'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> payload \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">remove\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'remove'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> payload\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> id \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CartAction\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-class\">ReturnType\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">typeof\u003C\u002Fspan> reset\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">|\u003C\u002Fspan> \u003Cspan class=\"shj-class\">ReturnType\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">typeof\u003C\u002Fspan> add\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">|\u003C\u002Fspan> \u003Cspan class=\"shj-class\">ReturnType\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">typeof\u003C\u002Fspan> remove\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F { type: 'reset' } | { type: 'add'; payload: Item } | { type: 'remove'; payload: string }\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">cartReducer\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CartState\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CartAction\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CartState\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">switch\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">case\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'add'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> items\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>items\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>payload\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">case\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'remove'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> items\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>items\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">filter\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>i\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> i\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> action\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>payload\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-func\">assertNever\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>action\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F ← now a compile error if a case is missing\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">assertNever\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>x\u003Cspan class=\"shj-type\">: never\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-type\">: never\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">throw\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Error\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`Unhandled action: \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>x\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-num\">METHODS\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'GET'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'POST'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-num\">METHODS\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'PUT'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Error: Property 'push' does not exist on type 'readonly [\"GET\", \"POST\"]'\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">logAllowedMethods\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>methods\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>methods\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">join\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">', '\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-func\">logAllowedMethods\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">METHODS\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Error: Argument of type 'readonly [\"GET\", \"POST\"]' is not assignable\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F to parameter of type 'string[]'. The type 'readonly [\"GET\", \"POST\"]'\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F is 'readonly' and cannot be assigned to the mutable type 'string[]'.\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">logAllowedMethods\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>methods\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">readonly\u003C\u002Fspan> string\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F* ... *\u002F\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-func\">logAllowedMethods\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">METHODS\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F fine — the parameter now admits it won't mutate\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F or, if the function genuinely can't change:\n\u003C\u002Fspan>\u003Cspan class=\"shj-func\">logAllowedMethods\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>\u003Cspan class=\"shj-num\">METHODS\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F fine — a fresh, mutable copy\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":452},[]]