{"slug": "23-typescript-tools-for-making-software-explicit-in-the-ai-era", "title": "23 TypeScript Tools for Making Software Explicit in the AI Era", "summary": "A developer argues that AI-assisted development makes it crucial to make software assumptions explicit, and lists 23 TypeScript tools that help achieve this. The tools include Effect for explicit effects and errors, Zod for runtime data validation, and io-ts for typed data boundaries. The post emphasizes that explicit constraints are more valuable in the AI era because they reduce inference and improve verification.", "body_md": "In my previous articles, [I argued that AI is changing the role of constraints in software development](https://dev.to/remojansen/from-rigidity-to-explicitness-how-ai-changes-the-role-of-constraints-in-software-5cp5).\n\nFor a long time, we treated constraints as friction. Static types felt slower than dynamic code. Schemas felt restrictive compared to flexible data. Explicit workflows felt more cumbersome than letting an application decide what to do at runtime.\n\nBut AI changes the economics.\n\nWriting code is becoming cheaper. Understanding what the code is supposed to do is not. And verifying that generated code actually does what we intended is becoming one of the most important parts of software development.\n\nThis leads to a simple principle:\n\nThe more important an assumption is, the more valuable it is to make that assumption explicit.\n\nThis is particularly important with TypeScript. TypeScript already makes some things explicit, but the type system cannot express everything. It cannot tell us what happens when an HTTP request fails, validate JSON received from an external service, tell us which application states are legal, describe database relationships, enforce module boundaries, or define how a distributed workflow should behave after a process crashes.\n\nThose things are often left implicit.\n\nThat is exactly where AI-assisted development becomes difficult. If a constraint exists only in someone's head, a prompt, a convention, or an undocumented assumption, the AI has to infer it. And inference is exactly where we don't want critical business rules to live.\n\nThe interesting thing about the TypeScript ecosystem is that there are now tools for making almost every layer of a system more explicit.\n\nHere are 23 of them.\n\n[Effect](https://effect.website/) makes effects, errors, dependencies, concurrency, resources, and schemas explicit.\n\nWithout Effect, we might write:\n\n``` js\nasync function getUser(id: string) {\n  const response = await fetch(`/users/${id}`);\n\n  if (!response.ok) {\n    throw new Error(\"Request failed\");\n  }\n\n  return response.json();\n}\n```\n\nThere is a lot of implicit information here. The function performs I/O. It can fail. It returns unvalidated external data. It depends on `fetch`\n\n. The caller has to discover all of this by reading the implementation.\n\nWith Effect, those concerns become part of the program's structure:\n\n``` js\nconst getUser = (id: string) =>\n  Effect.gen(function* () {\n    const response = yield* HttpClient.get(`/users/${id}`);\n    return yield* decodeUser(response);\n  });\n```\n\nThe important difference is not syntax. It is information density. The code communicates what the operation does, what it can fail with, what it depends on, and how it composes with other effects.\n\nEffect turns invisible operational behavior into explicit program structure.\n\n[Zod](https://zod.dev/) makes runtime data validation explicit.\n\nWithout it:\n\n``` js\nconst user = await response.json();\n\nsendEmail(user.email);\n```\n\nThe programmer is implicitly assuming that the response contains an `email`\n\nproperty.\n\nWith Zod:\n\n``` js\nconst User = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string().email()\n});\n\nconst user = User.parse(await response.json());\n\nsendEmail(user.email);\n```\n\nNow the assumption is explicit. The schema can be read by a human, used by the application, tested automatically, and inspected by AI.\n\nTypes describe what we believe. Runtime schemas verify what we actually received.\n\n[io-ts](https://gcanti.github.io/io-ts/) makes the boundary between unknown runtime data and typed data explicit.\n\nWithout a codec:\n\n``` js\ntype Payment = {\n  id: string;\n  amount: number;\n};\n\nconst payment: Payment = await getPayment();\n```\n\nThe type says `payment`\n\nis valid, but the runtime data has not actually been checked.\n\nWith io-ts:\n\n``` js\nconst Payment = t.type({\n  id: t.string,\n  amount: t.number\n});\n\nconst result = Payment.decode(await getPayment());\n```\n\nNow there is an executable description of the boundary between unknown data and trusted data. The codec can also be tested against invalid inputs.\n\n[Valibot](https://valibot.dev/) makes runtime validation and schemas explicit with a lightweight API.\n\nWithout it:\n\n```\nfunction createAccount(input: any) {\n  // Assume input is valid.\n}\n```\n\nWith Valibot:\n\n``` js\nconst AccountInput = v.object({\n  name: v.string(),\n  email: v.pipe(v.string(), v.email()),\n  age: v.pipe(v.number(), v.integer(), v.minValue(18))\n});\n\nfunction createAccount(input: unknown) {\n  const account = v.parse(AccountInput, input);\n}\n```\n\nThe requirements are no longer hidden inside the implementation. They are represented as data that humans, tests, tools, and AI can inspect.\n\n[TypeBox](https://sinclairzx81.github.io/typebox/) makes JSON Schema and TypeScript type definitions explicit and connected.\n\nWithout it:\n\n```\ninterface CreateOrder {\n  productId: string;\n  quantity: number;\n}\n```\n\nThe TypeScript type describes the application, but external validators and JSON Schema consumers need another representation.\n\nWith TypeBox:\n\n``` js\nconst CreateOrder = Type.Object({\n  productId: Type.String(),\n  quantity: Type.Integer({ minimum: 1 })\n});\n```\n\nThe schema becomes a first-class artifact that can drive validation, tooling, documentation, and generation.\n\nOne explicit contract is better than five independently maintained descriptions of the same contract.\n\n[XState](https://stately.ai/docs/xstate) makes states, events, transitions, and actors explicit.\n\nWithout a state machine:\n\n```\nif (loading) {\n  // ...\n}\n\nif (error) {\n  // ...\n}\n\nif (data) {\n  // ...\n}\n```\n\nAs the application grows, combinations can appear that were never intended. The actual state model exists implicitly in conditionals.\n\nWith XState:\n\n``` js\nconst machine = createMachine({\n  initial: \"idle\",\n\n  states: {\n    idle: {\n      on: { SUBMIT: \"submitting\" }\n    },\n\n    submitting: {\n      on: {\n        SUCCESS: \"success\",\n        FAILURE: \"failure\"\n      }\n    },\n\n    success: {},\n    failure: {\n      on: { RETRY: \"submitting\" }\n    }\n  }\n});\n```\n\nNow the possible states and transitions are explicit. An AI does not need to infer which transitions are legal. Tests can enumerate them, tooling can visualize them, and invalid transitions can be rejected.\n\nThis is the same idea I explored in my article about AI agents and finite state machines: don't ask the AI to infer the workflow when you can give it the workflow.\n\n[Prisma](https://www.prisma.io/) makes database models, relationships, migrations, and generated data access types explicit.\n\nWithout a schema-first ORM:\n\n``` js\nconst users = await db.query(\n  \"SELECT * FROM users WHERE organisation_id = ?\",\n  [organisationId]\n);\n```\n\nThe relationship between users and organisations is hidden in the database.\n\nWith Prisma:\n\n```\nmodel User {\n  id             String       @id\n  email          String\n  organisationId String\n  organisation   Organisation @relation(fields: [organisationId], references: [id])\n}\n\nmodel Organisation {\n  id    String @id\n  users User[]\n}\n```\n\nThe model and relationship are explicit, and the schema can generate types and migrations.\n\n[Drizzle ORM](https://orm.drizzle.team/) makes database schemas, SQL relationships, and query types explicit in TypeScript.\n\nWithout it:\n\n``` js\nconst result = await db.query(\n  \"SELECT id, name FROM users WHERE active = true\"\n);\n```\n\nThe SQL is explicit, but the relationship between the query and its TypeScript result is not.\n\nWith Drizzle:\n\n``` js\nconst users = await db\n  .select({\n    id: usersTable.id,\n    name: usersTable.name\n  })\n  .from(usersTable)\n  .where(eq(usersTable.active, true));\n```\n\nThe schema, query, selected fields, and result type can all be connected.\n\n[Kysely](https://kysely.dev/) makes SQL queries and their result types compile-time checked.\n\nWithout it:\n\n``` js\nconst result = await db.query(`\n  SELECT id, email\n  FROM users\n  WHERE organisation_id = $1\n`, [organisationId]);\n```\n\nThe AI has to infer the result shape from the SQL.\n\nWith Kysely:\n\n``` js\nconst result = await db\n  .selectFrom(\"user\")\n  .select([\"id\", \"email\"])\n  .where(\"organisation_id\", \"=\", organisationId)\n  .execute();\n```\n\nThe query is constrained by the database type. Invalid tables or columns can become compile-time errors.\n\nThe best constraint is often the one that fails automatically.\n\n[Pulumi](https://www.pulumi.com/) makes infrastructure resources, dependencies, and configuration explicit as typed code.\n\nWithout infrastructure-as-code, the architecture might be a collection of console settings, scripts, environment variables, documentation, and tribal knowledge.\n\nWith Pulumi:\n\n``` js\nconst bucket = new aws.s3.Bucket(\"uploads\");\n\nconst policy = new aws.s3.BucketPolicy(\"uploads-policy\", {\n  bucket: bucket.id,\n  policy: ...\n});\n```\n\nInfrastructure relationships become part of the program. The AI can inspect them, deployment tooling can inspect them, and infrastructure can be previewed before it changes.\n\n[AWS CDK](https://aws.amazon.com/cdk/) makes cloud infrastructure and its relationships explicit as TypeScript constructs.\n\nWithout CDK, the relationship between an API and Lambda might exist only in cloud configuration.\n\nWith CDK:\n\n``` js\nconst api = new apigateway.RestApi(this, \"Api\");\n\nconst handler = new lambda.Function(this, \"Handler\", {\n  runtime: lambda.Runtime.NODEJS_22_X,\n  handler: \"index.handler\",\n  code: lambda.Code.fromAsset(\"lambda\")\n});\n\napi.root.addMethod(\"GET\", new apigateway.LambdaIntegration(handler));\n```\n\nThe relationship is now explicit, reviewable, synthesizable, and testable.\n\n[dependency-cruiser](https://github.com/sverweij/dependency-cruiser) makes module dependency rules explicit and enforceable.\n\nImagine:\n\n```\nUI\n ↓\nApplication\n ↓\nDomain\n ↓\nInfrastructure\n```\n\nBut nothing prevents:\n\n```\nDomain\n ↓\nInfrastructure\n```\n\nWith dependency-cruiser, rules such as \"domain must not depend on infrastructure\" become executable.\n\nAn AI can read the rules, CI can enforce them, and generated imports that violate them can fail automatically.\n\nArchitectural decisions should be executable whenever possible.\n\n[Madge](https://github.com/pahen/madge) makes module dependency graphs and circular dependencies explicit.\n\nWithout tooling, a cycle such as this can be difficult to see:\n\n```\nA → B → C → D → A\n```\n\nWith Madge:\n\n```\nmadge --circular src/\n```\n\nthe graph becomes inspectable and circular dependencies can be detected automatically.\n\n[eslint-plugin-boundaries](https://github.com/j5ik2o/eslint-plugin-boundaries) makes architectural layer and module boundaries explicit and enforceable.\n\nWithout it:\n\n``` js\nimport { UserRepository } from \"../../infrastructure/database\";\n```\n\nmight compile perfectly even when application code is not supposed to access infrastructure directly.\n\nWith boundaries configured, the rule can become:\n\n```\napplication → domain\napplication → infrastructure ❌\ndomain → infrastructure ❌\n```\n\nESLint can reject the violation. The AI does not need to remember the architectural rule because the tool enforces it.\n\n[tRPC](https://trpc.io/) makes client/server procedure contracts explicit and type-safe.\n\nWithout a shared contract:\n\n```\napi.post(\"/users\", {\n  name,\n  email\n});\n```\n\nThe server might expect completely different field names. The contract exists implicitly across two implementations.\n\nWith tRPC:\n\n``` js\nconst createUser = publicProcedure\n  .input(\n    z.object({\n      name: z.string(),\n      email: z.string().email()\n    })\n  )\n  .mutation(({ input }) => {\n    // ...\n  });\n```\n\nThe procedure and its input contract become part of the program. The client can consume that contract directly, and the compiler provides feedback when it changes.\n\n[ts-rest](https://ts-rest.com/) makes HTTP endpoints, parameters, payloads, and responses explicit as shared contracts.\n\nWithout it:\n\n```\nfetch(\"/api/orders\", {\n  method: \"POST\",\n  body: JSON.stringify(order)\n});\n```\n\nThe AI has to infer what the endpoint expects, what it returns, and which status codes are possible.\n\nWith a contract:\n\n``` js\nconst contract = c.router({\n  createOrder: {\n    method: \"POST\",\n    path: \"/orders\",\n    body: CreateOrder,\n    responses: {\n      201: Order,\n      400: ErrorResponse\n    }\n  }\n});\n```\n\nThe HTTP boundary becomes explicit and can drive server implementation, client usage, tests, and documentation.\n\n[oRPC](https://orpc.unnoq.com/) makes RPC procedures and their client/server contracts explicit and type-safe.\n\nWithout a contract:\n\n```\nclient.createUser(...)\n```\n\nThe implementation determines what arguments are accepted.\n\nWith an explicit procedure contract, inputs, outputs, errors, and procedure identity become machine-readable. The AI is no longer asked to figure out how the API probably works; it is asked to implement against a defined contract.\n\n[OpenAPI Initiative](https://www.openapis.org/) makes network API contracts explicit independently of implementation language.\n\nWithout OpenAPI, an API might be described by a README:\n\n```\nPOST /users\n\nProbably takes:\n{\n  name,\n  email\n}\n\nReturns a user.\n```\n\nWith OpenAPI:\n\n```\npaths:\n  /users:\n    post:\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateUser'\n      responses:\n        '201':\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n```\n\nThe API becomes a formal artifact. Humans can read it, machines can validate it, tools can generate clients, and AI can consume it.\n\n[Orval](https://orval.dev/) makes OpenAPI contracts executable by generating strongly typed clients.\n\nWithout generation, an AI might repeatedly write:\n\n```\nfetch(\"/api/users/123\")\n```\n\nand have to infer the method, parameters, request body, response type, and error handling.\n\nWith Orval, the OpenAPI specification becomes the source from which client code is generated. The AI can use generated code whose structure already reflects the contract.\n\nExplicitness is even more powerful when it can be converted into automation.\n\n[Proto.Actor](https://proto.actor/) makes actors, messages, supervision, identity, and distributed communication explicit.\n\nWithout an actor model:\n\n```\nawait sendMessage(node, message);\n```\n\nWhat happens when the node disappears? Who owns the state? Should the message be retried? Who supervises the failure? Can two messages be processed concurrently?\n\nAn actor model gives these concepts explicit names and structures:\n\n```\nActor\n  ↓\nMessage\n  ↓\nState\n  ↓\nSupervision\n  ↓\nFailure handling\n```\n\nThe distributed system now has a vocabulary that both humans and AI can reason about.\n\n[Dapr](https://dapr.io/) makes distributed-system capabilities explicit through abstractions such as actors, state, pub/sub, and service invocation.\n\nWithout a distributed abstraction:\n\n```\nawait redis.set(key, value);\nawait kafka.publish(topic, message);\nawait fetch(serviceUrl);\n```\n\nThe distributed semantics are scattered throughout the application.\n\nWith Dapr, concepts such as state, pub/sub, service invocation, and actors become explicit architectural capabilities. The AI can reason about intent instead of reconstructing architecture from arbitrary infrastructure calls.\n\n[Temporal](https://temporal.io/) makes durable workflows, activities, retries, timers, failures, and long-running execution explicit.\n\nWithout Temporal:\n\n```\nawait chargeCard();\nawait createOrder();\nawait sendEmail();\n```\n\nWhat happens if the process crashes after `chargeCard()`\n\n? Should it be retried? What if `createOrder()`\n\nfails? Should the email be sent twice?\n\nWith Temporal:\n\n```\nawait chargeCard();\n\nawait proxyActivities<typeof activities>({\n  startToCloseTimeout: \"1 minute\",\n  retry: {\n    maximumAttempts: 3\n  }\n}).createOrder();\n\nawait sleep(\"1 day\");\n```\n\nThe workflow, retries, timers, activities, and durability semantics become explicit.\n\nThis is especially important for AI agents.\n\nLet AI decide what requires intelligence. Let the workflow engine handle what requires reliability.\n\n[fp-ts](https://gcanti.github.io/fp-ts/) makes functional effects, optionality, errors, and composition explicit through types.\n\nWithout it:\n\n```\nfunction findUser(id: string): User | undefined {\n  // ...\n}\n```\n\nThe caller has to remember to handle the missing case.\n\nWith explicit functional structures:\n\n``` js\nconst findUser = (id: string): Option<User> => {\n  // ...\n};\n```\n\nOr:\n\n``` js\nconst createUser = (\n  input: CreateUser\n): Either<ValidationError, User> => {\n  // ...\n};\n```\n\nThe possibility of failure is no longer an informal convention. It is part of the function's type.\n\nIf a function returns `User`\n\n, the AI may assume it always has a user. If it returns `Either<ValidationError, User>`\n\n, the failure path is visible and the compiler can help verify that it was handled.\n\nThese tools look very different. Effect is not Prisma. Prisma is not XState. XState is not OpenAPI. OpenAPI is not Temporal.\n\nBut they are all solving a similar problem.\n\nThey take something that would otherwise be implicit and give it a representation.\n\n| Implicit | Explicit |\n|---|---|\n| Errors | Effect / `Either`\n|\n| External data assumptions | Zod / io-ts / Valibot |\n| JSON Schema | TypeBox |\n| Application state | XState |\n| Database relationships | Prisma / Drizzle |\n| SQL result types | Kysely |\n| Infrastructure relationships | Pulumi / CDK |\n| Module architecture | dependency-cruiser / Madge |\n| Architectural boundaries | eslint-plugin-boundaries |\n| API contracts | tRPC / ts-rest / oRPC / OpenAPI |\n| Generated API clients | Orval |\n| Actor semantics | Proto.Actor / Dapr |\n| Durable workflows | Temporal |\n| Optionality and errors | fp-ts |\n\nThe important word here is **representation**.\n\nIf a rule only exists in a developer's head, there is very little an AI can do with it. If the rule exists only in a prompt, the AI can forget it. If it exists only in documentation, it can become stale.\n\nBut if the rule exists in code, a schema, a state machine, a contract, a dependency rule, or a workflow definition, it becomes part of the system.\n\nAnd once it is part of the system, we can automate around it.\n\nConsider an AI-generated change.\n\nWithout explicit constraints, verification might look like this:\n\n```\nAI generated code\n        ↓\nHuman reads it\n        ↓\nHuman tries to understand intent\n        ↓\nHuman guesses whether assumptions are correct\n        ↓\nTests\n```\n\nThere is a lot of interpretation involved.\n\nNow consider a system with explicit contracts:\n\n```\nAI generated code\n        ↓\nTypeScript compiler\n        ↓\nSchema validation\n        ↓\nAPI contract tests\n        ↓\nArchitecture rules\n        ↓\nState-machine tests\n        ↓\nDatabase constraints\n        ↓\nWorkflow verification\n```\n\nThe AI still generates code, but it generates code inside a much smaller space of possibilities.\n\nThat is the real advantage.\n\nWe don't need AI to become perfectly reliable. We can make the environment around AI more verifiable.\n\nThese tools don't only prevent mistakes. They improve the context available to the AI.\n\nSuppose I ask an AI:\n\nAdd a new payment provider.\n\nIn an implicit codebase, the AI has to discover how payments work, where providers live, which errors are possible, how payment state is represented, how retries work, which modules can depend on which, which API contract is expected, and how the database represents payments.\n\nIt has to reconstruct all of that.\n\nNow imagine the same request in an explicit system. The AI can inspect the payment state machine, payment schema, payment API contract, dependency rules, database schema, and workflow definition.\n\nThe problem has become much smaller.\n\nThe AI isn't necessarily smarter. The system is simply giving it better information.\n\nThis is why I increasingly think about constraints as **context compression**.\n\nAn explicit model can communicate a large amount of intent in a small, machine-readable representation.\n\nWhen something is implicit, testing often requires testing the implementation.\n\nWhen something is explicit, we can often test the model.\n\nWith an FSM we can test:\n\n```\nCan Submitted → Purchased happen directly?\n```\n\nWith an API contract:\n\n```\nDoes POST /orders return 201 with an Order?\n```\n\nWith a schema:\n\n```\nDoes invalid email data get rejected?\n```\n\nWith dependency rules:\n\n``` python\nCan domain import infrastructure?\n```\n\nIt should not.\n\nWith a database schema:\n\n```\nCan an Order reference a non-existent User?\n```\n\nIt should not.\n\nWith Temporal:\n\n```\nWhat happens when Activity #2 fails?\n```\n\nWith OpenAPI:\n\n```\nDoes the implementation conform to the published API?\n```\n\nThe verification target moves from \"Does this code look correct?\" toward \"Does this implementation satisfy these explicit constraints?\"\n\nThat is a much better problem for automation.\n\nThere is an obvious danger here.\n\nIf explicitness is good, it is tempting to make everything explicit.\n\nThat would be a mistake.\n\nNot every function needs an effect system. Not every application needs a state machine. Not every database needs an ORM. Not every project needs five architecture enforcement tools.\n\nThe goal is not maximum constraint. The goal is to make **important assumptions explicit**.\n\nA useful question is:\n\nIf this assumption were wrong, would the resulting bug be expensive?\n\nIf the answer is yes, it is a good candidate for explicit representation.\n\nIf the answer is no, inference may be perfectly reasonable.\n\nThe objective is not rigidity. It is explicitness where explicitness creates value.\n\nThis is what I find interesting about the current TypeScript ecosystem.\n\nIt is no longer just a language with a type checker. There are tools for making almost every important boundary explicit:\n\n```\n             Business process\n                    │\n                 XState\n                    │\n              Application\n                    │\n          ┌─────────┴─────────┐\n          │                   │\n       Effect              fp-ts\n          │                   │\n          └─────────┬─────────┘\n                    │\n                TypeScript\n                    │\n       ┌────────────┼────────────┐\n       │            │            │\n     Zod         tRPC        OpenAPI\n       │            │            │\n       └────────────┼────────────┘\n                    │\n              Database\n                    │\n       Prisma / Drizzle / Kysely\n                    │\n              Infrastructure\n                    │\n             Pulumi / CDK\n                    │\n          Distributed Systems\n                    │\n      Temporal / Dapr / Actors\n```\n\nAnd around all of it:\n\n```\ndependency-cruiser\nMadge\neslint-plugin-boundaries\n```\n\nThese tools are doing something bigger than adding features to TypeScript.\n\nThey are making assumptions observable.\n\nBefore AI-assisted development, a developer could often compensate for implicitness through experience.\n\nA senior developer might know that you shouldn't import infrastructure from the domain, that a particular API actually returns 202 rather than 200, that an operation is not safe to retry, that a state can only transition after approval, or that a database field is nullable even though the TypeScript type says otherwise.\n\nThat knowledge existed in people's heads.\n\nAI doesn't reliably have access to that knowledge. And even when we tell it, we have to trust that it will remember and apply it consistently.\n\nThis creates a new architectural pressure:\n\nMove important knowledge out of people's heads and into artifacts that machines can inspect and verify.\n\nSchemas. Types. Contracts. State machines. Dependency rules. Database models. Workflow definitions. Infrastructure definitions.\n\nThese become part of the AI's context. But more importantly, they become part of the system's verification surface.\n\nAI is making code generation increasingly cheap. That changes what we should optimize for.\n\nWe should spend less time asking:\n\nHow can I make writing this code faster?\n\nand more time asking:\n\nHow can I make it obvious whether this code is correct?\n\nThe tools in this list answer that question in different ways. Some make data explicit. Some make behaviour explicit. Some make architecture explicit. Some make infrastructure explicit. Some make distributed execution explicit. Some make APIs explicit. And some make failure explicit.\n\nThey all reduce the amount of important information that exists only through inference.\n\nThat is valuable for humans. But I think it is becoming even more valuable for AI.\n\nI don't think the future of software is going to be about adding more and more constraints.\n\nIt is about putting constraints in the right places.\n\nA good system might look like this:\n\n```\nAI\n │\n │ intelligence\n ▼\nExplicit contracts\n │\n ├── Types\n ├── Schemas\n ├── APIs\n ├── State machines\n ├── Architecture rules\n ├── Database models\n └── Workflows\n │\n ▼\nAutomated verification\n```\n\nThe AI remains probabilistic. The system around it becomes increasingly deterministic.\n\nThat is the architectural shift I find most interesting.\n\nWe don't need to make AI deterministic. We need to stop asking AI to infer things that software can already define.\n\nThe best TypeScript tools for the AI era may therefore not be the ones that help us write code faster. They may be the ones that make the **intent behind the code impossible to misunderstand**.\n\nBecause when code generation is cheap, explicitness becomes leverage.\n\nAnd when verification is the bottleneck, every explicit constraint becomes another thing a human or a machine can check.\n\nDon't make the AI remember your rules. Make your system represent them.\n\nThat is how we move from AI-generated software to AI-generated software that we can actually verify.", "url": "https://wpnews.pro/news/23-typescript-tools-for-making-software-explicit-in-the-ai-era", "canonical_source": "https://dev.to/remojansen/23-typescript-tools-for-making-software-explicit-in-the-ai-era-20hb", "published_at": "2026-08-21 08:53:59+00:00", "updated_at": "2026-08-21 09:14:33.095131+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["TypeScript", "Effect", "Zod", "io-ts"], "alternates": {"html": "https://wpnews.pro/news/23-typescript-tools-for-making-software-explicit-in-the-ai-era", "markdown": "https://wpnews.pro/news/23-typescript-tools-for-making-software-explicit-in-the-ai-era.md", "text": "https://wpnews.pro/news/23-typescript-tools-for-making-software-explicit-in-the-ai-era.txt", "jsonld": "https://wpnews.pro/news/23-typescript-tools-for-making-software-explicit-in-the-ai-era.jsonld"}}