Skip to content
aviral gupta

// B1.5 · ~45 min · Beginner

Build: type-check an existing JavaScript script

After this lesson you can take a working JavaScript script, rename it to main.ts, fix every diagnostic tsc reports under strict settings, and show that it still prints exactly the same.

Lesson 5 of 5 in B1 Setup and the basics

End of the module

You will be able to

  • Convert a script: rename it, run npx tsc, and compare the output of node main.ts with the JavaScript version
  • Annotate the parameters and return types that tsc reports, and fix the errors those annotations reveal
  • Fix values JavaScript converted silently (parseInt, isNaN, text in arithmetic) without changing the output
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: which of these does a JavaScript program run without any complaint? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: this JavaScript prints 37. Renamed to main.ts, how many diagnostics does npx tsc print?

    function percent(part, total) {
      return part / total * 100;
    }
    console.log(parseInt(percent(3, 8)));
  3. Practice · Activity 3 of 7

    tsc reported Parameter 'scores' implicitly has an 'any' type. Fill in the type: scores is looped over, each item is added to a number, and the call passes [92, 88, 95].

    function average(scores: ____): number {
      let sum = 0;
      for (const score of scores) {
        sum += score;
      }
      return sum / scores.length;
    }
    
    console.log(average([92, 88, 95]).toFixed(1));
    function average(scores: ): number {
  4. Practice · Activity 4 of 7

    tsc reports TS7006 for price and qty on line 1. You change line 1 to function lineTotal(price: number, qty: number): number { and run npx tsc again. What does it report now?

    function lineTotal(price, qty) {
      return price * qty;
    }
    const prices = [2.5, 1.2, 4];
    const qtys = ["2", "5", "1"];
    let sum = 0;
    for (let i = 0; i < prices.length; i++) {
      sum += lineTotal(prices[i], qtys[i]);
    }
    console.log("Sum:", sum.toFixed(2));
  5. Practice · Activity 5 of 7

    The JavaScript version printed 37. Which new line 4 makes tsc report no errors and still prints 37?

    function percent(part: number, total: number): number {
      return part / total * 100;
    }
    console.log(parseInt(percent(3, 8)));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. tsc reports Parameter 'name' implicitly has an 'any' type. You write name: any instead of name: string. How many errors does npx tsc report now?

    function greet(name) {
      return "Hi " + name.toUppercase();
    }
    console.log(greet("Ada"));
  7. Apply · Activity 7 of 7

    Mini-task. This JavaScript prints Words: 5, Longest: checking and Over the limit: false. Save that output, put the code in main.ts and run npx tsc. Annotate both parameters and both return types, run tsc again, and fix the diagnostic that appears then. Finish when tsc prints nothing and node main.ts prints the same three lines.

    function countWords(text) {
      return text.split(" ").length;
    }
    
    function longest(words) {
      let best = "";
      for (const word of words) {
        if (word.length > best.length) best = word;
      }
      return best;
    }
    
    const line = "type checking catches mistakes early";
    const limit = "5";
    console.log("Words:", countWords(line));
    console.log("Longest:", longest(line.split(" ")));
    console.log("Over the limit:", countWords(line) > limit);

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A weather log, converted

As main.js, this script printed exactly the output below. Renamed to main.ts, tsc reported three diagnostics: main.ts(1,23): error TS7006 for celsius, main.ts(5,18): error TS7006 for values, and main.ts(20,40): error TS2345 for parseInt(mean), because parseInt expects text. The fixes were number and number[] on the parameters, number as both return types, and Math.trunc(mean), which drops the decimals as parseInt did. The variables needed no annotations: TypeScript infers them. Run npx tsc, which prints nothing, then node main.ts, and compare.

main.ts

function toFahrenheit(celsius: number): number {
  return celsius * 9 / 5 + 32;
}

function average(values: number[]): number {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum / values.length;
}

const readings = [18.5, 21, 23.25, 19];
const days = ["Mon", "Tue", "Wed", "Thu"];
for (let i = 0; i < readings.length; i++) {
  console.log(days[i], readings[i], "C is", toFahrenheit(readings[i]), "F");
}
const mean = average(readings);
console.log("Average:", mean.toFixed(2), "C");
console.log("Whole degrees:", Math.trunc(mean));

Run it with

npx tsc
node main.ts

Output

Mon 18.5 C is 65.3 F
Tue 21 C is 69.8 F
Wed 23.25 C is 73.85 F
Thu 19 C is 66.2 F
Average: 20.44 C
Whole degrees: 20
  • The output is identical to what main.js printed: the annotations are removed before Node.js runs the file.
  • readings, days, mean and the loop variables have no annotations; TypeScript inferred number[], string[] and number.
  • Math.trunc(20.4375) is 20, the same as parseInt gave, so the last line did not change.
  • The line numbers of the three diagnostics are the same in main.js and main.ts: only types were added.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 3

Annotate the converted functions

This was main.js; it has just been renamed to main.ts. It works, but tsc reports TS7006 four times. Annotate every parameter and every return type. Do not change anything else: the tests call the functions and check that the program still prints Berlin: 69.8 F.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    celsius is multiplied and divided, so it is a number. values is looped over and summed: number[].

  2. Hint 2

    city is joined into text and degrees has toFixed called on it: string and number.

  3. Hint 3

    Each return type goes after the parameter list: function label(city: string, degrees: number): string {

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function toFahrenheit(celsius: number): number {
  return celsius * 9 / 5 + 32;
}

export function average(values: number[]): number {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum / values.length;
}

export function label(city: string, degrees: number): string {
  return city + ": " + degrees.toFixed(1) + " F";
}

console.log(label("Berlin", toFahrenheit(average([18, 21, 24]))));
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function toFahrenheit(celsius) {
  return celsius * 9 / 5 + 32;
}

export function average(values) {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum / values.length;
}

export function label(city, degrees) {
  return city + ": " + degrees.toFixed(1) + " F";
}

console.log(label("Berlin", toFahrenheit(average([18, 21, 24]))));

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {toFahrenheit, average, label} from './main.ts';

test('toFahrenheit(100) is 212', () => {
  assert.equal(toFahrenheit(100), 212, 'toFahrenheit(100) should be 212');
});

test('average([1, 2, 3, 4]) is 2.5', () => {
  assert.equal(average([1, 2, 3, 4]), 2.5, 'average([1, 2, 3, 4]) should be 2.5');
});

test('label("Oslo", 50) is Oslo: 50.0 F', () => {
  assert.equal(label('Oslo', 50), 'Oslo: 50.0 F', 'label("Oslo", 50) should be Oslo: 50.0 F');
});

test('the program still prints Berlin: 69.8 F', async () => {
  const out = (await runMain()).trimEnd();
  assert.equal(out, 'Berlin: 69.8 F', `the program printed ${JSON.stringify(out)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 3

Fix what JavaScript converted silently

The parameters are annotated already, and the program prints 4 true false 42, which is right. tsc still reports three errors, each a place where JavaScript converted a value by itself. Fix each one so that it type-checks, the functions keep their results and the program prints exactly the same. Leave the annotations as they are.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    parseInt expects text. Math.trunc(x) drops the decimals of a number, as parseInt did here.

  2. Hint 2

    isNaN expects a number. Convert the text on purpose first: isNaN(Number(text)).

  3. Hint 3

    text * 2 multiplies text. Number(text) * 2 says what JavaScript did silently.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function wholePages(bytes: number, perPage: number): number {
  return Math.trunc(bytes / perPage);
}

export function isNumber(text: string): boolean {
  return !isNaN(Number(text));
}

export function double(text: string): number {
  return Number(text) * 2;
}

console.log(wholePages(5000, 1024), isNumber("12"), isNumber("abc"), double("21"));
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function wholePages(bytes: number, perPage: number): number {
  return parseInt(bytes / perPage);
}

export function isNumber(text: string): boolean {
  return !isNaN(text);
}

export function double(text: string): number {
  return text * 2;
}

console.log(wholePages(5000, 1024), isNumber("12"), isNumber("abc"), double("21"));

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {wholePages, isNumber, double} from './main.ts';

test('wholePages keeps only whole pages', () => {
  assert.equal(wholePages(5000, 1024), 4, 'wholePages(5000, 1024) should be 4');
  assert.equal(wholePages(2048, 1024), 2, 'wholePages(2048, 1024) should be 2');
});

test('isNumber tells numbers from other text', () => {
  assert.equal(isNumber('12'), true, 'isNumber("12") should be true');
  assert.equal(isNumber('abc'), false, 'isNumber("abc") should be false');
});

test('double("21") is 42', () => {
  assert.equal(double('21'), 42, 'double("21") should be the number 42');
});

test('the program still prints 4 true false 42', async () => {
  const out = (await runMain()).trimEnd();
  assert.equal(out, '4 true false 42', `the program printed ${JSON.stringify(out)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 3 of 3

Build: type-check the grade report

The module build. This grade report was main.js; it prints seven correct lines. Make it type-check under the course tsconfig without changing a single character of its output. Run npx tsc (or the tests) and fix the first diagnostic, then run again. Expect seven diagnostics at first, and one more after you annotate grade. Annotate every parameter and return type, and fix each mismatch where the value comes from. The tests compare the output with the JavaScript version.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Start with the four TS7006 errors: scores is number[], avg is a number, name is a string. Add return types too: number, string, string.

  2. Hint 2

    avg >= passMark compares a number with "50", and bonus * 1 multiplies "5". Store both values as numbers where they are declared.

  3. Hint 3

    parseInt(classAverage) becomes Math.trunc(classAverage). After avg: number, grade("100") is reported: pass the number 100.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Grade report: each student's average, then a summary.
function average(scores: number[]): number {
  let sum = 0;
  for (const score of scores) {
    sum += score;
  }
  return sum / scores.length;
}

function grade(avg: number): string {
  if (avg >= 85) return "A";
  if (avg >= 70) return "B";
  if (avg >= 50) return "C";
  return "F";
}

function formatLine(name: string, avg: number): string {
  return name.padEnd(6) + avg.toFixed(1) + "  " + grade(avg);
}

const names = ["Ada", "Grace", "Linus"];
const scores = [[92, 88, 95], [71, 64, 80], [45, 52, 38]];
const passMark = 50;
const bonus = 5;

let passed = 0;
let classTotal = 0;
for (let i = 0; i < names.length; i++) {
  const avg = average(scores[i]);
  classTotal += avg;
  console.log(formatLine(names[i], avg));
  if (avg >= passMark) passed++;
}
const classAverage = classTotal / names.length;
console.log("Passed:", passed, "of", names.length);
console.log("Class average, whole points:", Math.trunc(classAverage));
console.log("With bonus:", classAverage + bonus * 1);
console.log("A perfect 100 is grade", grade(100));
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

// Grade report: each student's average, then a summary.
function average(scores) {
  let sum = 0;
  for (const score of scores) {
    sum += score;
  }
  return sum / scores.length;
}

function grade(avg) {
  if (avg >= 85) return "A";
  if (avg >= 70) return "B";
  if (avg >= 50) return "C";
  return "F";
}

function formatLine(name, avg) {
  return name.padEnd(6) + avg.toFixed(1) + "  " + grade(avg);
}

const names = ["Ada", "Grace", "Linus"];
const scores = [[92, 88, 95], [71, 64, 80], [45, 52, 38]];
const passMark = "50";
const bonus = "5";

let passed = 0;
let classTotal = 0;
for (let i = 0; i < names.length; i++) {
  const avg = average(scores[i]);
  classTotal += avg;
  console.log(formatLine(names[i], avg));
  if (avg >= passMark) passed++;
}
const classAverage = classTotal / names.length;
console.log("Passed:", passed, "of", names.length);
console.log("Class average, whole points:", parseInt(classAverage));
console.log("With bonus:", classAverage + bonus * 1);
console.log("A perfect 100 is grade", grade("100"));

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';

const lines = async () => (await runMain()).trimEnd().split('\n');

test('the three student lines are unchanged', async () => {
  const out = (await lines()).slice(0, 3).join('\n');
  assert.equal(out, 'Ada   91.7  A\nGrace 71.7  B\nLinus 45.0  F', `the student lines were ${JSON.stringify(out)}`);
});

test('the summary lines are unchanged', async () => {
  const out = (await lines()).slice(3).join('\n');
  assert.equal(
    out,
    'Passed: 2 of 3\nClass average, whole points: 69\nWith bonus: 74.44444444444444\nA perfect 100 is grade A',
    `the summary lines were ${JSON.stringify(out)}`
  );
});

test('the report still has exactly seven lines', async () => {
  const count = (await lines()).length;
  assert.equal(count, 7, `the program printed ${count} lines`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

An array parameter annotated as a single number

function average(scores: number) {
  let sum = 0;
  for (const score of scores) {
    sum += score;
  }
  return sum / scores.length;
}
console.log(average(4));

What tsc or Node.js prints

main.ts(3,23): error TS2488: Type 'number' must have a '[Symbol.iterator]()' method that returns an iterator.

Why, and the fix

number is one number, and one number cannot be looped over with for...of: that needs an iterable, such as an array. tsc also reports that number has no length. The body loops over scores, so it is an array of numbers: scores: number[]. Then change the call to pass an array as well: average([4]).

A return type that does not match what the function returns

function label(avg: number): number {
  return avg >= 50 ? "pass" : "fail";
}
console.log(label(70));

What tsc or Node.js prints

main.ts(2,22): error TS2322: Type 'string' is not assignable to type 'number'.

Why, and the fix

The annotation after the parameter list says what the function returns, and tsc checks every return against it. Here both results are text, so tsc reports "pass" at column 22 and "fail" at column 31. The code is right and the annotation is wrong: write function label(avg: number): string.

Number with a capital N as the type

function double(n: Number) {
  return n * 2;
}
console.log(double(4));

What tsc or Node.js prints

main.ts(2,10): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

Why, and the fix

The type of a number is number, in lower case. The Handbook says the capitalised names String, Number and Boolean refer to special built-in types that very rarely appear in your code, and * does not accept a Number. Write n: number, and use string and boolean in lower case too.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Rename, check, compare

Converting a script starts with a rename: main.js becomes main.ts. Before you rename it, run node main.js and keep what it prints. Then run npx tsc. With strict on in the course tsconfig, expect a list of diagnostics: that is normal after a conversion. Work through them from the first, running npx tsc after each fix, until it prints nothing. Finally run node main.ts and compare with the saved output. Node.js removes the types and runs the rest, so annotations cannot change the output. If the output changed, one of your fixes changed the code itself.

Most diagnostics are parameters

A JavaScript parameter has no type, so TypeScript would treat it as any, which checks nothing. strict includes noImplicitAny, which makes that error TS7006: Parameter 'scores' implicitly has an 'any' type. Pick the type from the body and the calls: a parameter looped over and summed is number[], one with padEnd called on it is a string. It goes after the name; the return type goes after the parameter list: function average(scores: number[]): number. Variables mostly need nothing, because TypeScript infers them. After annotating, run tsc again: calls that were unchecked while the parameter was any, such as grade("100"), now get their own errors.

What JavaScript let slide

Some diagnostics are real mismatches that JavaScript converted silently: parseInt(69.4) expects text, isNaN("12") expects a number, "5" * 2 multiplies text, and avg >= "50" compares a number with text. The script printed the right thing anyway, and it must keep doing so. Store the value with the right type (const passMark = 50), convert on purpose with Number(text), or use the function made for the job: Math.trunc(69.4) drops the decimals as parseInt did. Then check with node main.ts, because a fix can type-check and still print something else: Math.round and toFixed(0) both turn 37.5 into 38.

Sources

Last reviewed September 30, 2026