Skip to main content

TypeScript React CI: Type-Check in Your Pipeline

TypeScript adds compile-time type checking to JavaScript, catching type mismatches before code runs. Running the TypeScript compiler (tsc) in your CI pipeline prevents unsafe type violations from reaching production. When a developer passes the wrong type to a function or accesses a non-existent property, the CI build fails and they get a clear error message to fix before merging.

Why Type-Check in CI?

Type checking is a form of automated testing that finds entire categories of bugs (null pointer exceptions, wrong argument types, missing properties) without writing a single test. Teams using TypeScript report 15-38% fewer production bugs (source: Stack Overflow 2023 Developer Survey). In CI, type checking acts as a safety net: it verifies every commit against your defined types and prevents untyped or incorrectly-typed code from reaching main.

Without CI type-checking, a developer might disable TypeScript locally, check in untyped code, and bypass all type safety. CI enforces that all code conforms to your type definitions.

Configuring TypeScript for React

Ensure TypeScript is installed in your React project:

npm install --save-dev typescript @types/react @types/react-dom

Create a tsconfig.json in your project root:

{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmitOnError": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

Key settings:

  • strict: true: Enables all strict type checking options (recommended for CI)
  • noEmitOnError: true: Prevents emitting JavaScript if type errors exist
  • jsx: "react-jsx": Enables JSX syntax in .tsx files

Add a type-check script to package.json:

{
"scripts": {
"type-check": "tsc --noEmit"
}
}

The --noEmit flag checks types without generating .js output, since your build step handles compilation.

Type-Checking in Your Workflow

Add a type-check job to your workflow:

name: React CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
type-check:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Run TypeScript type-check
run: npm run type-check

lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Run ESLint
run: npm run lint

build:
runs-on: ubuntu-latest
needs: [type-check, lint]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Build React app
run: npm run build

The build job now depends on both type-check and lint, ensuring builds only proceed after all checks pass.

Example: Type Error Detection

Suppose a developer writes a React component with a type error:

interface UserProps {
id: number;
name: string;
}

function UserCard({ id, name }: UserProps) {
return (
/* Error: name is string, but toUpperCase() needs to be called correctly */
<div>{name.toUppercase()}</div>
);
}

The TypeScript compiler would emit:

error TS2551: Property 'toUppercase' does not exist on type 'string'.

Running npm run type-check in CI would fail the job and prevent the code from merging, forcing the developer to fix the typo to toUpperCase().

Strict Mode vs. Standard Mode

TypeScript supports varying levels of strictness. Strict mode enforces the most comprehensive checks:

SettingDescriptionBenefit
strict: falseLoose checking, allows implicit anyEasier onboarding, legacy support
strict: trueAll strict options enabledMaximum type safety, catches more bugs
Per-rule strict settingsFine-grained controlGradual migration to strict

For new React projects, always use strict: true. For existing projects, consider using skipLibCheck: true to skip checking third-party library types while still enforcing strict checking on your code.

Handling Type Errors with Comments

Sometimes you need to suppress a type error intentionally (e.g., working with untyped third-party code). Use a TypeScript ignore comment:

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const data: any = JSON.parse(unknownInput);

Document why the suppression exists so future developers understand the intent.

Key Takeaways

  • Type-checking in CI catches type errors automatically, preventing unsafe code from reaching production
  • Use tsc --noEmit in a separate CI job so type errors are visible before builds
  • Enable strict: true in tsconfig.json to enforce comprehensive type safety
  • Order your CI jobs logically: type-check and lint before building, so failures are caught early
  • TypeScript types provide free, automated testing of function signatures and data shapes

Frequently Asked Questions

Can I type-check TypeScript without building?

Yes, use tsc --noEmit to check types without generating JavaScript output. This is fast and doesn't require your build step.

What if a library doesn't have type definitions?

Install the @types/library-name package if available (DefinitelyTyped has types for most popular libraries). If no types exist, you can either write .d.ts definition files or use skipLibCheck: true to ignore library type errors.

How do I suppress type errors in specific files?

Add a triple-slash comment at the top of the file:

// @ts-nocheck

This disables all type checking for that file. Use sparingly; document the reason.

Should I use any type?

Avoid any. It defeats the purpose of TypeScript. If a type is unknown, use unknown and narrow it with type guards:

// Bad: defeats type safety
const data: any = fetch(...);

// Good: requires narrowing before use
const data: unknown = fetch(...);
if (typeof data === 'string') {
console.log(data.toUpperCase());
}

Further Reading