{"id":"code-dev-ts-type-guard-no-libraries","audience":"developers","category":"coding","track":"coding","technique":"constraints","title":"Write a TypeScript type guard without a validation library","free":false,"task":"The webhook handler casts JSON.parse output straight to OrderEvent. Write isOrderEvent(value: unknown): value is OrderEvent with no imports or libraries (zod is banned in this edge bundle) that returns false instead of throwing. Your team's rules: amountCents must be an integer of 0 or more, and occurredAt must parse as a date (Date.parse not NaN). Exactly 1 code block of at most 16 lines, under 20 words of text.","materialLabel":"The types and the handler (attached to your prompt automatically)","material":"// src/types.ts  (TypeScript 5.5, Cloudflare Worker, Tallyway payments)\nexport type OrderStatus = \"created\" | \"paid\" | \"refunded\";\n\nexport interface OrderEvent {\n  id: string;\n  amountCents: number;\n  status: OrderStatus;\n  occurredAt: string;   // ISO 8601\n}\n\n// src/webhook.ts\nimport type { OrderEvent } from \"./types\";\n\nexport async function handleWebhook(req: Request): Promise<Response> {\n  const body = await req.text();\n  const event = JSON.parse(body) as OrderEvent;   // <- trusts anything\n  await recordEvent(event);\n  return new Response(\"ok\");\n}\n\n// Incidents this month:\n//  - Nov 2: a partner sent amountCents: \"1999\" (a string); totals broke\n//  - Nov 4: amountCents: 12.5 got stored and the ledger rounded it wrong\n//  - Nov 5: occurredAt: \"yesterday\" made the daily report crash\n//\n// Bundle limit for the worker is 1 MB; zod alone added 57 KB, so the\n// team said no runtime libraries in this package.","rules":[{"type":"codeBlocks","count":1},{"type":"maxCodeLines","max":16},{"type":"maxProseWords","max":20}],"mistakes":[{"title":"Reaching for zod","why":"The AI's first idea is a schema library, which this bundle can't afford.","fix":"Say: \"No imports or libraries; plain typeof checks.\"","checks":["r0"]},{"title":"A loose number check","why":"typeof x === 'number' still lets 12.5 through, the Nov 4 bug.","fix":"Add: \"amountCents must be Number.isInteger and >= 0.\"","checks":["r1"]},{"title":"Any string as a date","why":"Checking only typeof string lets 'yesterday' through.","fix":"Add: \"occurredAt must parse: !Number.isNaN(Date.parse(...)).\"","checks":["r2"]},{"title":"Extras around it","why":"The AI rewrites the handler too and explains type guards.","fix":"Add: \"Only the function, one code block, no text.\"","checks":["codeBlocks","maxCodeLines","maxProseWords"]}],"promptHints":[{"label":"Names the guard","patterns":["isOrderEvent","value is OrderEvent","type guard"]},{"label":"Rules out libraries","patterns":["no (imports|libraries|zod)","zod"]},{"label":"Checks integers","patterns":["integer","isInteger"]},{"label":"Checks the date","patterns":["Date\\.parse","valid date","NaN"]},{"label":"Asks for only the function","patterns":["only the function","just the function","no (text|explanation)"]}],"hint":"A guard is only as good as its checks, and the incidents tell you which ones matter. Tell the AI the signature, the no-library rule and the exact number and date rules.","rubricLabels":["Has the signature isOrderEvent(value: unknown): value is OrderEvent, checks value is a non-null object, and uses no imports or libraries","Requires amountCents to be an integer of 0 or more (e.g. Number.isInteger(v.amountCents) && v.amountCents >= 0), so '1999' and 12.5 are rejected","Requires occurredAt to be a string where Date.parse does not return NaN, and status to be one of created, paid or refunded"]}