Codex Creates a New TypeScript Interface Duplicating One That Already Exists

Codex defines a fresh User or ApiResponse type when an identical one lives elsewhere. How to make the agent search first via AGENTS.md, indexes, and ts-morph.

You review a Codex PR that adds a User interface inside src/auth/types.ts. Three problems: you already have User in src/types/user.ts, the new one has slightly different fields, and now half the codebase imports the old User and half the new one. TypeScript stops catching mismatches because they are structurally compatible enough to assign across.

Codex did not search before defining. It saw the local context, recognized the shape it needed, and inlined a fresh declaration. Without a forcing function — an explicit “search first” rule, a shared types module the agent knows to look at, or a CI check that flags duplicate definitions — this happens on every refactor task.

Common causes

1. Agent did not grep before creating

Codex’s default workflow is “read the file I am editing, add what is missing.” If the missing piece is a type, it writes one inline. It rarely does a repo-wide search for an existing definition.

How to spot it: PR introduces a new interface X or type X = .... grep -rn "interface X\|type X " src/ shows another definition elsewhere.

2. Types are scattered across the repo with no central index

Half your types live in src/types/, the other half are colocated with their first user (src/auth/types.ts, src/api/types.ts). There is no obvious “where types live” answer. Codex shrugs and adds to the local file.

How to spot it: find src -name 'types.ts' -o -name 'types/*.ts' returns ten or more paths. No src/types/index.ts re-exporting them.

The agent guidance has no rule about reusing types. Codex picks the locally-convenient path.

How to spot it: grep -i 'type\|interface\|reuse' AGENTS.md returns nothing.

4. Existing type lives in a barrel the agent did not load

You have src/types/index.ts that re-exports everything, but the agent only opened the file it was editing. It never saw the barrel and assumed nothing existed.

How to spot it: Transcript shows read_file calls only on the immediate file. No grep / search across the repo for the type name.

5. Type names overlap with common library names

Codex saw Response and assumed it was the DOM Response. It created a fresh ApiResponse rather than checking if you already had one. Common types (User, Item, Response, Error, Config) collide most often.

How to spot it: Diff adds a generic-named type. Existing one has the same generic name.

Shortest path to fix

Step 1: Add a “search before creating” rule to AGENTS.md

## Type and interface reuse

Before defining a new `type`, `interface`, `class`, or `enum`:

1. Run `grep -rn "interface <Name>\b\|type <Name>\b\|class <Name>\b" src/`
2. If a definition exists, import it. Do not duplicate.
3. If the existing one is missing fields you need, extend it
   (`interface X extends BaseX`) or update it — do not fork.
4. New types belong in `src/types/<domain>.ts`. Do not inline new types
   in feature files unless the type is private to one module.

When a name is generic (`User`, `Item`, `Response`), search broadly — include
`packages/`, `apps/`, and `shared/`.

A rule the agent can actually follow: search command + decision tree.

Step 2: Centralize types in a single import path

Make src/types/index.ts a barrel that re-exports every shared type:

// src/types/index.ts
export type { User } from './user'
export type { Session } from './session'
export type { ApiResponse, ApiError } from './api'
export type { Permission } from './permission'

Then in AGENTS.md:

All shared types are exported from `@/types`. Import from there:

    import type { User } from '@/types'

If a type is not in `@/types`, it is module-private. Search `@/types` before
defining anything that could plausibly be reused.

A single import path makes “did this already exist” answerable in one grep.

Step 3: Add a ts-morph CI check for duplicate type names

// scripts/check-duplicate-types.ts
import { Project } from 'ts-morph'

const project = new Project({ tsConfigFilePath: 'tsconfig.json' })
const decls = new Map<string, string[]>()

for (const file of project.getSourceFiles('src/**/*.ts')) {
  for (const iface of file.getInterfaces()) {
    const name = iface.getName()
    const list = decls.get(name) ?? []
    list.push(file.getFilePath())
    decls.set(name, list)
  }
  for (const alias of file.getTypeAliases()) {
    const name = alias.getName()
    const list = decls.get(name) ?? []
    list.push(file.getFilePath())
    decls.set(name, list)
  }
}

const dupes = [...decls.entries()].filter(([, files]) => files.length > 1)
if (dupes.length > 0) {
  for (const [name, files] of dupes) {
    console.error(`Duplicate type ${name}:`)
    for (const f of files) console.error(`  ${f}`)
  }
  process.exit(1)
}

Wire to CI as a required check. Codex respects red CI.

Step 4: Suggest the existing type in the error message

If your codebase uses ESLint with custom rules, add a no-redeclare-shared-type rule that points at the canonical location. Even without custom rules, you can use eslint-plugin-import with no-duplicates and no-restricted-imports to keep imports flowing through @/types:

{
  "rules": {
    "no-restricted-imports": ["error", {
      "patterns": [{
        "group": ["**/auth/types", "**/api/types", "**/user/types"],
        "message": "Import shared types from @/types, not deep paths."
      }]
    }]
  }
}

Codex reads ESLint output and self-corrects.

Step 5: Review imports in agent PRs first

In your PR checklist:

- [ ] No new `interface` or `type` declarations duplicating something in `@/types`
- [ ] All new types go in `src/types/<domain>.ts`, not inline in feature files
- [ ] `npm run check:types` passes (runs the ts-morph duplicate check)

This is the human safety net for cases ts-morph cannot catch (slightly different shapes with the same name).

Prevention

  • AGENTS.md mandates “grep before define” with the exact grep command
  • All shared types live in src/types/ and re-export from src/types/index.ts
  • ts-morph CI check fails on duplicate interface/type names
  • ESLint no-restricted-imports keeps imports flowing through @/types
  • Reviewers spot-check new interface and type declarations in agent PRs
  • For generic names (User, Item, Response) require a longer prefix to avoid future collisions

Tags: #Codex #agent #Troubleshooting #types