A Passing Playwright Test Is Not a Type Check: Add `tsc` to Your Workflow
Set up a small Playwright TypeScript test and add a separate tsc --noEmit check for test files locally and in CI.

A passing Playwright test tells you that its browser interactions and assertions worked in that run. It does not tell you that TypeScript accepts the test file. Playwright transforms TypeScript to JavaScript and runs it without checking types, so keep a compiler command beside the browser command—not hidden behind an assumption that one does the other’s job. The Playwright TypeScript documentation makes that distinction explicit.
Start with one test directory
For a new project, npm init playwright@latest provides an initialization path. Choose TypeScript for the tests, then add TypeScript as a development dependency if the project does not already have it. The initializer and install commands are separate from the checks below:
npm init playwright@latest
npm install -D typescript
Keep the example configuration small. In playwright.config.ts, point Playwright at tests and give relative navigation an application URL:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'https://staging.example.com',
},
});
That URL is a placeholder. Use an application environment with a test account and a login flow that your team can exercise. The configuration does not create the application or the account.
Test the login result, not just the click
Suppose this application accepts a test account at /login and, after sign-in, opens /dashboard with a visible “Your dashboard” heading. Put this in tests/login.spec.ts, changing the account details and expected heading to match your application:
import { test, expect } from '@playwright/test';
test('signing in opens the dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('replace-with-test-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await expect(
page.getByRole('heading', { name: 'Your dashboard' }),
).toBeVisible();
});
test and expect come from Playwright Test. The callback receives its page fixture from Playwright; this test does not need to launch a browser or create a page manually. Label- and role-based locators describe the controls a user encounters, while the awaited assertions check both navigation and the resulting heading. Run the browser check with:
npx playwright test
If it passes, the sign-in interactions and both assertions passed in that run. It does not establish that every account or environment behaves the same way.
Select the tests for a separate compiler check
Add tests/tsconfig.json so the compiler command selects test files rather than relying on an application config that might include only src:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true
},
"include": ["./**/*.ts"]
}
Then run:
npx tsc -p tests/tsconfig.json --noEmit
A successful exit means the files selected by that configuration passed this static check without emitting JavaScript. It does not run the browser test. Conversely, Playwright’s ability to load the spec is not evidence that the compiler accepts it: when loading tests, Playwright supports only a limited set of tsconfig options, and it does not type-check the transformed code.
You can check that the compiler command is actually reaching your tests. Temporarily add this deliberately invalid line to tests/login.spec.ts:
const dashboardPath: string = 42; // Deliberate type error; remove after checking.
tsc should report a type error for the selected file. Playwright may still transform and run a test containing a non-critical TypeScript error, which is precisely why a green browser run cannot substitute for this check. Remove the line before committing.
Make both results visible in CI
Run the same two commands locally before pushing, and give each its own step or command in CI:
npx tsc -p tests/tsconfig.json --noEmit
npx playwright test
The first result concerns static checking under the test configuration. The second concerns the behavior exercised by the browser test in that run. If one fails, investigate that failure on its own terms: a type error is not a failed login, and a failed dashboard assertion is not proof of a type error. Keeping the checks separate makes that distinction visible when someone maintains the test next year.

