Your First TypeScript File

7 min read

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.ts

Output: 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 const or let: 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 User could 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 : any to silence a red squiggle. : any opts 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 when any is acceptable.)
  • Editing the compiled .js instead of the .ts. With outDir: "./dist", your edits should always go in src/*.ts. Changing dist/hello.js works once, and the next tsc run 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.

  1. In your ts-for-qa/src folder, create test-result.ts.
  2. Declare a testResult variable annotated with the shape { name: string; passed: boolean; duration: number } and fill in real values.
  3. Use a template literal and a ternary to print a one-line summary like Login test ✅ in 1250ms (or for failures).
  4. Compile with npx tsc (or run with npx ts-node src/test-result.ts) and confirm the output.
  5. 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 } (drop duration) and leave duration: 1250 in the value.
    • Add const ms: number = testResult.name;
  6. 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.

// tip to track lessons you complete and pick up where you left off across devices.