✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

EcosystemAdvanced6 min read

Strapi and TypeScript: compiling when there are type errors

September 9, 2026
Strapi and TypeScript: compiling when there are type errors

You are mid-refactor. You save a file. The dev server dies:

config/env/production/database.ts:3:47 - error TS1005: ';' expected.
Found 1 error(s).

You know about the error. You were going to fix it. You wanted to check something else first, and now you are restarting the server instead.

The frustration in the original forum thread is specific and reasonable: "I would like it to throw errors in production, but warnings in development." That is the right instinct, and TypeScript gives you the tools for it, just not with one switch.

The direct answer: noEmitOnError

By default, tsc already emits JavaScript even when there are type errors. noEmitOnError defaults to false. Type errors are reported and output is still written.

So if your build stops on a type error, either something set noEmitOnError: true, or the error is not a type error at all.

{
  "extends": "@strapi/typescript-utils/tsconfigs/server",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": ".",
    "noEmitOnError": false
  },
  "include": ["./", "src/**/*.json"],
  "exclude": ["node_modules/", "build/", "dist/", ".cache/", ".tmp/", "src/admin/", "**/*.test.ts", "src/plugins/**"]
}

The distinction that matters: syntax errors vs. type errors

This is the part that resolves most of the confusion.

Type errors (TS2322, TS2345, TS2349: "Type X is not assignable to type Y") are semantic. The compiler understands your code and disagrees with it. It can still emit JavaScript, and noEmitOnError: false lets it.

Syntax errors (TS1005, TS1109, TS1128: "';' expected") mean the compiler could not parse the file. There is no valid output to emit. No configuration setting rescues you; the code has to be fixed.

The error in the forum thread was TS1005: ';' expected on this line:

import parse = require('pg-connection-string').parse;

That is not valid TypeScript. import x = require(...) is TypeScript's CommonJS import form, and it does not accept a property access on the end. The fix:

import { parse } from 'pg-connection-string';

export default ({ env }) => {
  const config = parse(env('DATABASE_URL'));

  return {
    connection: {
      client: 'postgres',
      connection: {
        host: config.host,
        port: Number(config.port),
        database: config.database,
        user: config.user,
        password: config.password,
        ssl: { rejectUnauthorized: false },
      },
    },
  };
};

So the first thing to check when a build "will not compile despite my settings" is the error code. TS1xxx is syntax; nothing will make it emit.

Targeted escape hatches

When you need to move past an error you have decided not to fix right now, use the narrowest tool.

skipLibCheck: for third-party type problems

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

Skips type checking of .d.ts files. This is the correct fix when a dependency ships broken or conflicting type definitions, which is the case you genuinely cannot control. It is on by default in most modern setups and costs you nothing in your own code's safety. It also speeds up compilation noticeably on large dependency trees.

// @ts-expect-error: for a single line

// @ts-expect-error the provider's types are wrong; it does accept a string here
someLibrary.configure('value');

Prefer this over // @ts-ignore. @ts-expect-error fails the build if the line stops having an error, so when the library ships a fix, you find out and delete the comment. @ts-ignore silently rots.

Always write the reason on the same line. A bare suppression is unreviewable.

// @ts-nocheck: for a whole file

// @ts-nocheck

Turns off checking for the entire file. Reasonable for a generated file or a large chunk of migrated JavaScript you have not typed yet. Not reasonable as a habit: one @ts-nocheck on a controller silently removes checking from every future change to it.

allowJs: for incremental migration

{
  "compilerOptions": {
    "allowJs": true
  }
}

Lets .js and .ts files coexist. This is the documented approach for adding TypeScript to an existing JavaScript Strapi project, and it is a much better answer than suppressing errors: convert file by file, and leave the rest as JavaScript until you get to it.

Relaxing strictness

{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": false,
    "noImplicitAny": false
  }
}

Legitimate for a project mid-migration. Understand what you are turning off: strictNullChecks in particular is where most of TypeScript's value lives, because it catches the undefined dereferences that become runtime 500s. Turning it off to unblock a refactor is fine; leaving it off forever means you are paying TypeScript's costs without its main benefit.

The setup the thread actually wanted

Strict in production, forgiving in development. This is achievable, and the trick is to separate type checking from building.

1. Keep the base config strict:

{
  "extends": "@strapi/typescript-utils/tsconfigs/server",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": ".",
    "strict": true,
    "skipLibCheck": true,
    "noEmitOnError": false
  },
  "include": ["./", "src/**/*.json"],
  "exclude": ["node_modules/", "build/", "dist/", ".cache/", ".tmp/", "src/admin/", "**/*.test.ts", "src/plugins/**"]
}

noEmitOnError: false means a type error is reported but the dev server keeps running.

2. Make type checking a separate command:

{
  "scripts": {
    "develop": "strapi develop",
    "build": "strapi build",
    "start": "strapi start",
    "typecheck": "tsc --noEmit",
    "typecheck:watch": "tsc --noEmit --watch"
  }
}

Run npm run typecheck:watch in a second terminal. You get continuous type feedback in one pane and a running server in the other, with errors visible and nothing blocked.

3. Enforce it in CI:

name: CI
on: [push, pull_request]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: yarn
      - run: yarn install --frozen-lockfile
      - run: yarn typecheck

This is the shape that gives you what the thread asked for: the type system never blocks local iteration, and nothing with a type error reaches main.

Two Strapi-specific gotchas

Do not set noEmit: true

{
  "compilerOptions": {
    "noEmit": true   // ← breaks the Strapi build
  }
}

noEmit means "type check only, write nothing". Applied to your root tsconfig.json, strapi build type-checks and emits no dist/. The build reports success and the app fails at runtime with:

Cannot destructure property 'client' of 'db.config.connection' as it is undefined.

…because dist/config/database.js does not exist. Use tsc --noEmit as a command-line flag for type checking, never as a setting in the config Strapi builds with. See that error's guide for the full story.

Regenerate types after schema changes

Strapi generates types from your content-type schemas. After adding a field or a content type, they are stale until regenerated:

npx strapi ts:generate-types

A surprising share of "impossible" type errors in Strapi projects are just out-of-date generated types. Run this before you start suppressing anything.

If the generated types themselves produce errors you do not want to see, exclude them:

{
  "exclude": ["types/generated/**"]
}

Choosing the right tool

SituationUse
A dependency's types are wrongskipLibCheck: true
One line, one known problem// @ts-expect-error <reason>
A generated or legacy file// @ts-nocheck
Adding TS to a JS projectallowJs: true, convert incrementally
Dev server dying on type errorsnoEmitOnError: false + tsc --noEmit --watch
Enforcing qualityyarn typecheck in CI
Build succeeds but dist/ is emptyRemove noEmit: true
Types do not match your schemastrapi ts:generate-types
TS1xxx syntax errorFix the code; nothing else works

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

Seeding Data in Strapi: Why Migrations are the Wrong Tool
EcosystemIntermediate·7 min read

Seeding Data in Strapi: Why Migrations are the Wrong Tool

Migrations run before Strapi's schema sync, so seeding a brand-new content type fails on the missing table. Use bootstrap instead, idempotently.

·September 9, 2026
"Cannot read properties of undefined (reading 'attributes')" in Strapi
EcosystemIntermediate·6 min read

"Cannot read properties of undefined (reading 'attributes')" in Strapi

Strapi will not boot because a content type references a component or relation that does not exist. How to find the broken reference quickly.

·September 9, 2026
How to debug a 500 Internal Server Error in Strapi
EcosystemBeginner·7 min read

How to debug a 500 Internal Server Error in Strapi

A 500 means Strapi threw an exception it did not expect. The real message is in your server logs, not the browser. A systematic way to find it.

·September 9, 2026