Skip to main content
ANVISoftware Solutions
Lesson 17 of 20Intermediate17 min

TypeScript Basics

By the end of this lesson

Add types to JavaScript and read compiler errors confidently.

TypeScript is JavaScript with a description of what each value is meant to be. A checker reads your code before it runs and reports the places where the description and the use do not match.

Nothing is added at run time. The types are removed during the build, and the browser receives ordinary JavaScript. So TypeScript cannot check anything while your page is running — its entire contribution happens before that.

What you get for the effort is the class of bug that comes from a value not being what you assumed: a name that is sometimes missing, an amount that is really a string, a function called with its arguments the wrong way round.

Annotation and inference
TypeScript
// Annotated: you state the type
const department: string = "Finance";

// Inferred: the checker works it out from the value
let claimCount = 0;                                        // number
const categories = ["travel", "equipment", "training"];    // string[]

// Annotations earn their place on the edges of a function
function formatAmount(amount: number): string {
  return "GBP " + amount.toFixed(2);
}

formatAmount(48.5);     // fine
formatAmount("48.5");   // Error before it runs: string is not assignable to number

claimCount = "three";   // Error: claimCount was inferred as number
  • A colon after a name introduces its type. string, number and boolean cover most values.
  • The annotation on department is redundant. The checker can see it is a string, and writing it out gives you nothing while giving you something to keep in step.
  • Inference works on more than simple values. categories is inferred as string[] — an array of strings — so pushing a number into it is an error.
  • The parameter and return annotations on formatAmount do carry weight. They are the contract, and they are checked at every call site.
  • The two errors at the bottom are found without running anything. In plain JavaScript the first produces "48.5".toFixed is not a function at run time, if that line is ever reached.
  • Reassigning claimCount to text is an error even though nothing was annotated. Inference is not weaker than annotation — it produces the same type, from the value instead of from you.

Where to annotate, and where to let inference do it:

  • Annotate function parameters — inference cannot see how a function will be called
  • Annotate function return types on anything non-trivial. It catches the case where the body accidentally returns the wrong thing, and pins the contract before the body is written
  • Annotate an empty array or object you are about to fill, since there is nothing to infer from
  • Annotate a value that arrives from outside your code as unknown, so you are forced to check it
  • Let inference handle local variables initialised with a value — the annotation is duplication
  • Let inference handle the return of a short arrow function whose result is obvious

Type errors can be long, especially with nested objects. They are not as bad as they look, because they are written outside-in and the useful part is at the end. Read from the bottom up:

  1. Start with the last line

    The final "Type X is not assignable to type Y" is the actual mismatch. Everything above it is the path the checker took to get there — property by property, from the value you passed down to the part that did not fit.

  2. Identify X and Y

    X is what you have. Y is what was expected. Read it in that order every time: "I supplied X, it wanted Y." This one habit removes most of the confusion, because the two are easy to swap round.

  3. Follow the path back up

    The lines above tell you where in the structure the mismatch lives. "Types of property 'status' are incompatible" means the objects match except for status, so you can ignore everything else.

  4. Fix the cause, not the message

    There are three honest fixes: the value is wrong and needs changing, the type is wrong and needs widening, or the value genuinely might be missing and needs a check. Adding any is not on the list.

  5. Hover before you guess

    In an editor, hovering over a name shows the type the checker has worked out for it. Comparing that with what you expected usually locates the mistake faster than reading the error again.

The vocabulary you need to read most annotations:

string, number, boolean
The everyday primitives. Lowercase — the capitalised versions are something else and are not what you want.
Employee[]
An array of Employee. Array<Employee> means the same thing.
null and undefined
Separate types under strict mode, which is what forces you to handle the absent case.
string | null
A union: either a string or null. The vertical bar means or, and you must narrow before using it as a string.
"draft" | "approved"
Literal types. The value must be one of those exact strings, so a typo in a status is a compile error rather than a row that never matches.
unknown
Could be anything, and you must check before use. The right type for data crossing into your code.
any
Could be anything, and no checking is done. Keep for migration of existing JavaScript, not for new code.
void
The function returns nothing useful. Common on event handlers.

Summary

  • TypeScript checks your code before it runs and disappears from the output
  • Annotate function parameters and returns; let inference handle initialised locals
  • Strict mode separates null and undefined from other types, which is most of the value
  • Read type errors from the bottom up: the last line is the real mismatch
  • any removes checking and spreads; unknown keeps the question open and makes you answer it

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Add types to working JavaScript

Take this function and type it, assuming strict mode: function findManagerName(employees, employeeId) { const employee = employees.find((e) => e.id === employeeId); return employee.managerName; }

The checker will report a problem. Fix it properly rather than silencing it, and decide what the function should return when there is no answer.

Show solution

find returns Employee | undefined, because it may not find anything. Under strict mode, reading a property of that is an error — and it is a real one. This function throws at run time for any id that is not in the list.

The return type has to admit the possibility. string | undefined is honest and pushes the decision to the caller, who knows what to display. Throwing is also defensible when a missing employee means a bug rather than a normal case.

managerName is optional in the type, because the most senior person has no manager. That makes it string | undefined as well, so both absences are visible in the signature — and the caller cannot forget either.

Silencing this with any would have produced code that compiles and still crashes. The error was a genuine defect report, not an obstacle.

TypeScript
interface Employee {
  id: string;
  name: string;
  department: string;
  managerName?: string;   // the most senior employee has no manager
}

function findManagerName(
  employees: Employee[],
  employeeId: string
): string | undefined {
  const employee = employees.find((candidate) => candidate.id === employeeId);

  if (!employee) {
    return undefined;
  }

  return employee.managerName;
}

// The caller now has to deal with both absences, which is the point
const managerName = findManagerName(employees, "E-00417") ?? "No manager on record";

Think about it

Think about it

A teammate says TypeScript gives false confidence, because the types are gone at run time so bad data still gets through. Where are they right, and where are they wrong?

Show solution

They are right about data entering the application. A type is a statement about what you expect, and nothing checks it against an actual API response. That is the subject of the last lesson in this module, and it needs a run-time check.

They are wrong about code you wrote. Inside your own application the checker verifies that every call, property access and assignment is consistent with the declared types, and it does that across every file on every build — including the code paths your testing never reaches.

So the accurate position is that TypeScript checks your code, not your data. Validate at the boundary, and let the checker cover everything inside it.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

What does TypeScript check at run time?
Why is unknown a better choice than any for a value whose type you do not know?
A long type error ends with "Type 'string' is not assignable to type 'number'". Where do you start reading?

Saved in this browser only.