You've completed the migration. Before moving on, it's worth stepping back to assess what you've built, reflect on what the process taught you, and establish the practices that will keep the codebase healthy going forward. This lesson covers the self-assessment checklist, the stretch goals that take a migrated project to the next level, and the maintenance habits that prevent a TypeScript project from drifting back toward untyped territory over time.
Self-assessment checklist
Work through this list. If any item is unchecked, treat it as an open task before considering the capstone done.
Infrastructure:
- All
.jsfiles incypress/have been renamed to.ts -
cypress.config.tsreplacescypress.config.js -
npm run type-checkexits with zero errors - CI pipeline runs
npm run type-checkbeforecypress run
Type coverage:
- All domain interfaces defined (
User,Product,Order,CartItem) - All custom commands declared in
cypress.d.tswith precise signatures -
process.envvariables typed inenv.d.ts - All
cy.fixture<>()calls use the generic overload with a named interface - No implicit
any(withnoImplicitAny: true)
Page objects:
- All
Locatorproperties declared asprivate readonlyin the constructor - All methods have explicit return types (
void,Cypress.Chainable<T>) - No
element.click()whereelementisany
Test files:
- All five test files pass type-check and
cypress run - No
// @ts-ignorecomments without an explanatory comment alongside - No
// @ts-expect-errorcomments that are no longer needed (runnpm run type-checkand check for "unused suppression" errors)
Documentation:
-
MIGRATION.mdexists and covers: TypeScript version, strict flags enabled, how to add a new test file, where custom command types live
Reflection
Take ten minutes to write answers to these questions. The act of writing makes the lessons concrete — you're more likely to apply them on the next migration.
Which stage took the longest? For most projects, Stage 5 (test files) takes the most calendar time because there are more files and each one needs a test run after conversion. Stage 7 (strict mode) takes the most concentrated debugging time. If you found Stage 3 (page objects) the hardest, it may indicate that the original JS page objects had more implicit typing assumptions than expected.
What bugs did TypeScript catch during migration? List them specifically. Property typos? Wrong argument order on a custom command? A test that passed because an assertion ran against undefined? These bugs are the concrete ROI of the migration. They're also the most compelling evidence when making the case for TypeScript to a sceptical colleague.
What would you do differently next time? Common answers: "write the type declarations before renaming files instead of during", "commit more frequently", "start with stricter settings and fix forward instead of starting permissive", "pick a smaller project to do first". There's no universally right answer — the point is identifying what created friction and eliminating it next time.
One thing that surprised you. TypeScript migration surprises are almost always that some assumption baked into the JavaScript was wrong — a function that was documented to return string but actually returned string | undefined in one code path, a custom command that worked but had parameters in the wrong order, a fixture field that changed shape in a recent API update.
Stretch goals
These are not required to complete the capstone — they represent the next level of TypeScript adoption. Tackle them in order; each one builds on the previous.
1. Zero explicit any annotations.
grep -rn ": any\|as any" cypress/ --include="*.ts"For each hit: can it be replaced with a specific type? With unknown and narrowing? The goal is zero. Common holdouts: Cypress.env() calls (use a typed wrapper), response.body accesses (use the response generic), JSON.parse() results (use unknown with Zod).
2. Add ESLint with @typescript-eslint.
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: { project: './tsconfig.json' },
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': 'error',
},
};no-explicit-any enforces the zero-any goal. no-floating-promises catches missing await in tests — a bug TypeScript's type system doesn't catch on its own.
3. Generate types from your API's OpenAPI spec.
If the application under test exposes a Swagger or OpenAPI spec, generate TypeScript interfaces from it automatically:
npm install --save-dev openapi-typescript
npx openapi-typescript https://your-api.com/openapi.json -o cypress/types/api.tsThis replaces hand-written interfaces like User and Order with types generated directly from the source of truth — the API spec. When the API changes, re-running the generator shows every test that breaks.
4. Branded types for IDs.
Database IDs that are typed as string allow accidental substitution — passing a productId where a userId is expected. Branded types prevent this:
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId): Promise<User> { /* ... */ }
const productId = createProductId('prod-123');
getUser(productId);
// Error: Argument of type 'ProductId' is not assignable to parameter of type 'UserId'For a test suite with many entity types, branded IDs eliminate an entire category of test data bugs.
5. Type tests with tsd.
tsd lets you write tests that verify your type declarations behave correctly:
npm install --save-dev tsd// cypress/types/index.test-d.ts
import { expectType } from 'tsd';
import type { User } from './index';
const user: User = { id: 'u1', email: 'a@b.com', name: 'Alice', role: 'admin', createdAt: '2024-01-01' };
expectType<string>(user.id);
expectType<'admin' | 'member' | 'guest'>(user.role);Run npx tsd in CI. If a future refactor changes User.id from string to number, the type test fails immediately.
- – New files always .ts
- – PRs: convert touched .js files
- – Track any count weekly
- – Zero explicit any
- – ESLint + @typescript-eslint
- – OpenAPI type generation
- – Compile-time bug detection
- – Safe refactoring across 13 files
- – Self-documenting test suite
- TypeScript for QA — deeper patterns –
- Test Automation Frameworks –
- API Testing Masterclass –
Maintenance going forward
A migrated codebase drifts back toward untyped code without explicit team agreements. Four practices that prevent this:
New files are always .ts. Non-negotiable from day one of the migration. Add a CI check if the team is large: find cypress -name "*.js" | grep -v node_modules | wc -l should return zero.
PRs that touch a .js file include its conversion. If someone is editing legacy-helper.js anyway, the marginal cost of converting it is low. The team builds this habit into code review: "you're already here, rename it."
Weekly any count. Run grep -rn ": any\|as any" cypress/ --include="*.ts" | wc -l and track the number in your team's dashboard or weekly sync. A rising count is a signal that someone is taking shortcuts under time pressure. A falling count is progress worth celebrating.
Document TypeScript conventions. Add a brief TypeScript section to your team's testing guidelines. Decisions worth documenting: when to use interface vs type, when explicit return types are required vs inferred, how to handle any holdouts (comment required), which ESLint rules are enforced.
What you've learned
This course started with the question: should we migrate? It ends with a fully typed Cypress project and the skills to repeat the process on any JavaScript test suite.
The skills you've built are transferable. The migration sequence — tsconfig, types layer, page objects, support files, tests, config, strictness — applies to Playwright projects, Jest API test suites, and any other JavaScript testing codebase. The principles — migrate leaf nodes first, commit every rename separately, tighten strictness incrementally, prefer explicit types over suppression — compound over every subsequent migration.
What you've built is also evidence. The bugs TypeScript caught during migration are bugs that would have reached CI, staging, or production if you had continued in JavaScript. The next time someone asks "is TypeScript worth the effort?", you have a specific, concrete answer.
Next steps
From here, the courses that build directly on this foundation:
- TypeScript for QA — deeper TypeScript patterns: generics, utility types, conditional types, and advanced page object typing
- Test Automation Frameworks — architectural patterns for large test suites, now that your codebase is typed enough to use them safely
- API Testing Masterclass — contract testing and schema validation, which TypeScript's type system enhances significantly