You have TypeScript installed, a tsconfig.json in place, and ts-node ready to go. Time to write code and feel the difference. This lesson takes you through your first .ts file end to end — a basic greeting, a deliberate type error, and a realistic test result object — and shows what TypeScript catches that JavaScript would have shipped.
Hello, TypeScript
Inside the src/ folder of the project you set up last lesson, create hello.ts:
const greeting: string = "Hello from TypeScript!";
console.log(greeting);Compile and run:
npx tsc && node dist/hello.js…or, in one step:
npx ts-node src/hello.tsOutput: Hello from TypeScript!. No surprises — yet.
Triggering your first type error
Now break it on purpose. Change the line to:
const greeting: string = 42;VS Code underlines greeting in red the moment you save. Hover over it and you'll see the message:
Type 'number' is not assignable to type 'string'.
Run the build:
npx tsc
# error TS2322: Type 'number' is not assignable to type 'string'.The compiler refuses to emit. There is no dist/hello.js to run — TypeScript stopped you before broken code reached Node.js. In JavaScript for QA you'd have happily assigned a number to a variable named greeting and watched downstream code blow up at runtime; here, the mistake is caught before the file is saved to disk.
A realistic QA example
A bare string is fine for a "hello world." Real test code shuffles objects around. Here's a test result object — the kind of value a custom reporter might build:
const testResult: { name: string; passed: boolean; duration: number } = {
name: "Login test",
passed: true,
duration: 1250,
};
console.log(`${testResult.name}: ${testResult.passed ? "✅" : "❌"} (${testResult.duration}ms)`);Output: Login test: ✅ (1250ms).
The annotation : { name: string; passed: boolean; duration: number } describes the shape of the object — three required fields with their types. This inline shape is fine for one-off objects; in chapter 3 you'll learn interface for reusing shapes across files.
What TypeScript catches that JavaScript won't
Add a typo on the next line:
console.log(testResult.status);
// ❌ Property 'status' does not exist on type
// '{ name: string; passed: boolean; duration: number; }'.In JavaScript, testResult.status would silently return undefined — the test would log undefined and keep going, and you'd debug it later wondering why the report column was blank. TypeScript catches the typo the second you type it.
The same protection covers wrong assignments:
testResult.passed = "yes";
// ❌ Type 'string' is not assignable to type 'boolean'.
testResult.duration = "1250ms";
// ❌ Type 'string' is not assignable to type 'number'.Anywhere JavaScript would coerce a value silently and let you keep going, TypeScript stops and tells you exactly which type you violated.
The annotation pattern
The whole syntax is : type after a name. That's it.
- After a
constorlet:const port: number = 8080; - After a function parameter:
function login(user: string) { ... } - After a function's parameter list, for the return type:
function getUrl(): string { ... } - Inside an object type:
{ name: string; passed: boolean }
If you remove every : ... annotation from a TypeScript file, what's left is plain JavaScript. The type layer is purely additive — it never changes how the code runs.
Autocomplete that actually knows
Save the file with testResult declared. In your editor, type:
testResult.The autocomplete drop-down shows name, passed, duration — and only those three. No more guessing whether the field is userName or username, isLoggedIn or loggedIn. Your editor reads the type and offers exactly what's available. Misremembering a property name was one of the biggest sources of silent test bugs in JavaScript; in TypeScript the editor refuses to let you write the wrong name in the first place.
Predict what compiles
Will it compile? Match each line to its outcome.
Each snippet runs against the testResult object: { name: string; passed: boolean; duration: number }. Drag each line to whether the TypeScript compiler accepts or rejects it.
- testResult.name = "Logout test";
- testResult.passed = "true";
- testResult.status = "flaky";
- console.log(testResult.duration + 100);
- const next: string = testResult.duration;
Three errors here are the everyday savings: "true" (a string) isn't a boolean; status isn't on the type at all; duration (a number) can't be assigned into a string variable. JavaScript would have run all three without complaint.
TypeScript at runtime is just JavaScript
After tsc compiles hello.ts, the resulting hello.js is plain JavaScript:
const greeting = "Hello from TypeScript!";
console.log(greeting);Annotations are erased. Types exist only at compile time — the runtime world is unchanged. This is why you can drop TypeScript into an existing Node project, a Cypress suite, or a Playwright project without rewriting any logic. You're adding a check, not swapping languages.
⚠️ Common mistakes
- Forgetting that types vanish at runtime. TypeScript doesn't validate values that come in at runtime — an API response cast as
Usercould be missing fields and TS won't know. Annotations protect your code from your team's mistakes; runtime validation (with libraries like Zod, or explicit checks) is what protects you from bad data on the wire. - Writing
: anyto silence a red squiggle.: anyopts that variable out of all type checking. The error goes away because the type system stopped looking. Almost always the wrong move — fix the actual mismatch instead. (Lesson 4 covers whenanyis acceptable.) - Editing the compiled
.jsinstead of the.ts. WithoutDir: "./dist", your edits should always go insrc/*.ts. Changingdist/hello.jsworks once, and the nexttscrun silently overwrites it. Confused beginners spend an hour wondering why their changes "won't stick."
🎯 Practice task
Write and break a real TypeScript file. 15-25 minutes.
- In your
ts-for-qa/srcfolder, createtest-result.ts. - Declare a
testResultvariable annotated with the shape{ name: string; passed: boolean; duration: number }and fill in real values. - Use a template literal and a ternary to print a one-line summary like
Login test ✅ in 1250ms(or❌for failures). - Compile with
npx tsc(or run withnpx ts-node src/test-result.ts) and confirm the output. - Now break it deliberately, one change at a time. After each, run the compiler and read the error message before reverting:
- Set
testResult.passed = "yes"; - Add
console.log(testResult.statusCode); - Change the annotation to
{ name: string; passed: boolean }(dropduration) and leaveduration: 1250in the value. - Add
const ms: number = testResult.name;
- Set
- Stretch: add a fourth field,
tags: string[](an array of strings), populate it with["smoke", "auth"], and log them joined with commas.
Each error message you read is one TypeScript catches for you in real projects. The next lesson formalises annotations vs inference — when to write the type and when to let the compiler figure it out.