· 14 min read

The Only TypeScript Article You'd Ever Need - 1/4

Welcome (Back)

If you’ve been following my series on becoming an actual developer, good job on completing JS. As mentioned in my last part of the JS article, I hope you came in here after building a solid project. Put it on your resume if you haven’t already. Feel free to send it to me in any of my socials, I’d appreciate it :)

Our journey doesn’t end here though. We’re not even a tenth of the way through. Now, here’s the uncomfortable truth: the deeper you dive into JS, the more you realize something is fundamentally wrong with this picture.

It’s time to admit it: your JavaScript code is a house of cards. A beautiful but fundamentally unstable house of cards.

JavaScript, The Language Built in 10 Days

JS was famously pieced in together in ten days. Ten days. That’s roughly the amount of time it takes most people like you (yes, you) to decide to shower, let alone create a programming language that would eventually power 99% of the internet. It was designed to make images dance on a page, not to build enterprise-grade applications. Yet, here we are, clinging to it.

There’s no sheriff in town here, variables change types easily, functions accept any garbage you throw at them, and objects births new properties like unexpected crazy. It’s flexible, they say. It’s dynamic, they praise. What it really is, is a license to shoot yourself in the foot.

Remember that TypeError: undefined is not a function? Or Cannot read properties of null (reading 'map')? That’s not just a bug, that’s JS, whispering sweet nothings into your ear before gutting your application at runtime. It’s the programming equivalent of finding out that your crucial calculateTax function decided to interpret “50 dollars” as “5” and “0” and then add them. Because, well, “dynamic typing”

You, undeservingly, should get something better. Your users deserve better.


Statically Typed vs. Dynamically Typed

We’ve talked about this before, but let’s hammer it home.

Dynamically Typed Languages (think JS, Python, Ruby):**

  • The Promise: “Freedom…Flexibility…Write code fast, without declaring types”
  • The Reality: “I’m going to accept any data type you give me and try my best to make it work. If it blows up, it’ll be at runtime, and you’ll have no idea why until you trace through 50 layers of your codebase. Good luck debugging.” It’s like a restaurant where you can just walk into the kitchen and throw any ingredients you want into a pot. The chef (the JS engine) will try to cook it. Maybe it’ll be a delicious meal. Maybe it’ll be a something radioactive. You only find out after you’ve served it to the customer.

Statically Typed Languages (think Java, C++)

  • The Promise: “Structure, safety, errors caught before deployment”
  • The Reality: “You will tell me precisely what kind of data each variable holds. You will tell me what types of arguments your functions expect and what type they return. If you violate these rules, I will scream at you (with a compilation error) and refuse to run your code. You will curse me now, but thank me later when your software doesn’t implode on impact.” Everything is labeled. Every dish has a precise recipe. The error is caught before the food even gets near the customer.

Being “flexible” in JS means being flexible enough to shoot yourself in the foot at any time. Static typing stops preventable mistakes before they happen. It’s like wearing a seatbelt, even though you think you’re a good driver. Because sometimes, the other guy is drunk, or your own code is having a stroke.


Enter TypeScript

How do we make it behave without rewriting the entire internet? TypeScript.

TypeScript isn’t a new language that replaces JS. It’s a superset of JavaScript. It’s JS with a strict older sibling. Any valid JS code is also valid TypeScript code. TypeScript just adds rules, type definitions, and a compilation step to the party.

It’s a:

  • Weird Linter: It alerts you about incorrect data types, missing properties, and functions being called with the wrong arguments.
  • Documentation in Disguise: When you define types, you’re implicitly documenting your code. No more guessing what someMysteriousObject.data holds. It’s either a string, a number, or it’s clearly telling you what went wrong.
  • Refactoring Enforcer: Ever tried to rename a function or a property in a large JavaScript codebase? It’s a game of “find all references and pray.” With TS, your IDE will hold your hand and tell you exactly where you’ve broken things.
  • Future-Proofing: TS often implements future JS features (like decorators or optional chaining) before they’re widely supported in browsers, allowing you to use them today.

You define your ingredients, you define your recipes, and the compiler (tsc) makes sure you don’t try to make a cake with a shoe.


There’s Always a Catch

Okay, before you commit entirely to the TS lifestyle, let’s acknowledge that it’s not a magical mf. There are trade-offs, and if you’re not aware, you’ll just swap one set of problems for another.

  • The Compilation Step: This is the biggest hurdle for JS natives. Your .ts files don’t run directly in the browser or Node.js. They first need to be transpiled (converted) into plain (vanilla) JS. This adds an extra step to your development workflow. It means a build tool (like Webpack, Rollup, or just the TypeScript compiler itself) is now a mandatory part of your life. Get used to it.
  • Type-Wrestling: Sometimes, TypeScript’s strictness will feel like trying to fit a fat square pig in a tiny round hole. There will be scenarios where the type system seems to fight your perfect JS logic. This is part of the learning curve. Embrace the struggle, for it builds character (and better code).
  • Initial Setup Overhead: Getting a TS project off the ground can be more time consuming than just throwing a .js file into an HTML page. You need Node.js, npm or yarn, the TypeScript package, and a tsconfig.json file. It’s a barrier to entry.

But still, I’d say that the benefits for larger, more maintainable, and less buggy codebases far outweigh the initial pain.


Preparing for the Ritual

Before we dive into TS, we need to set up our development environment properly.

Node.js & NVM

You probably already have Node.js installed. If you don’t, just google how you can download and set it up. But don’t just download it directly from the website. Use a Node Version Manager (nvm). Trust me, your future self will thank you when you need to switch between Node.js versions for different projects, because some legacy codebase from 2018 still insists on Node -20 or whatever.

Install NVM for Linux/macOS (for Windows, try nvm-windows or WSL):

# Download and run the install script (check nvm GitHub for latest version)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# Verify installation (restart your terminal if 'command not found')
nvm --version

Using NVM:

# Install a specific Node.js version (e.g., the latest LTS)
nvm install --lts

# Use that version
nvm use --lts

# Set it as default for new terminal sessions
nvm alias default 'lts/*'

# See what versions you have
nvm ls

# Switch between versions
nvm install 20 # Install Node.js 20
nvm use 20    # Switch to Node.js 20

The Package Manager

npm (Node Package Manager) is installed automatically with Node.js. It’s how you download, install, and manage all the external libraries and tools your project will depend on. It’s an essential tool in the JS ecosystem.

A Quick Overview

  • npm init: Initializes a new Node.js project, creating a package.json file. This file describes your project, its dependencies, and scripts.
  • npm install <package-name>: Installs a package locally to your node_modules directory and adds it to dependencies in package.json.
  • npm install -D <package-name> or npm install --save-dev <package-name>: Installs a package as a development dependency. These are tools you need for development (like TypeScript itself, testing frameworks, linters) but not for your final production application.
  • npm start, npm test, etc.: Runs scripts defined in your package.json.

Alternatives: pnpm, yarn

Yes, npm is the default, but it’s not always the best. Two alternatives address some of npm’s shortcomings (mostly around speed and disk space):

  • Yarn (Yet Another Resource Negotiator): Created by Facebook. Historically faster and offered better dependency resolution. Its yarn.lock file provided more consistent builds.
  • pnpm (Performant npm): It uses a content-addressable store to save disk space and drastically speed up installations by hard-linking packages. It’s often the fastest and most efficient for monorepos.

Should you use them?

  • For simple projects/beginners: npm is perfectly fine. The differences won’t be critical.
  • For larger projects, monorepos, or when every second counts: Investigate pnpm. It’s generally considered the best performance-wise right now.
  • If you’re already in a project that uses yarn: Stick with yarn. Avoid mixing package managers in the same project.

For this series, I’ll mostly use npm commands since it would relate to a larger audience, but I personally recommend trying pnpm.

Setting Up a TS Project

Let’s get our barebones TS project up and running.

# 1. Create a new directory for your project
mkdir my-ts-project
cd my-ts-project

# 2. Initialize a new npm project
npm init -y # The -y flag answers 'yes' to all prompts, speeding things up.

# 3. Install TypeScript as a development dependency
npm install --save-dev typescript

# 4. Initialize TypeScript configuration
# This creates the tsconfig.json file.
npx tsc --init

That npx tsc --init command just created tsconfig.json, the brain of your TypeScript project.

tsconfig.json

This file tells the TypeScript compiler (tsc) how to behave. It dictates what files to include, what JavaScript version to compile to, how strict to be with types, and a million other things that will make your head spin.

Open tsconfig.json. It’s heavily commented. Here are the most important lines to focus on initially:

{
  "compilerOptions": {
    /* Language and Environment */
    "target": "es2016" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', 'ES2022', 'ESNext'. */,
    "lib": [] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', or 'nodenext'. */ /* Modules */,
    "rootDir": "./src" /* Specify the root folder within your source files. */,
    "outDir": "./dist" /* Specify an output folder for all emitted files. */,
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */,

    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */ /* Emit */ /* Advanced */, // "sourceMap": true,               /* Create source map files for emitted JavaScript files. */ // "declaration": true,             /* Create .d.ts files for emitted JavaScript for each TypeScript or JavaScript file. */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}

Some description of the common ones:

  • target: This is where you tell TypeScript what JavaScript version your compiled code should be. If you’re targeting older browsers (e.g., IE11, god forbid), you might set it to "es5". For modern Node.js environments, "es2020" or "esnext" is fine.
    • Example: If target is "es5", const and let will be transpiled to var. Arrow functions will become regular functions.
      • TS input: const greet = (name: string) => console.log(Hello, ${name});
      • JS output (target: "es5"): var greet = function (name) { console.log("Hello, " + name); };
  • module: This defines the module system for your compiled JavaScript.
    • "commonjs": The default for Node.js. Uses require() and module.exports.
    • "esnext" or "node16"/"nodenext": For modern ES Modules (import/export) in Node.js or browsers.
  • rootDir: Where your TypeScript source files live. Usually ./src.
  • outDir: Where TypeScript should put the compiled JavaScript files. Usually ./dist or ./build.
  • strict: SET THIS TO true AND LEAVE IT THERE. This single flag enables a host of strict type-checking options (noImplicitAny, noImplicitThis, strictNullChecks, etc.). It’s the difference between TypeScript being a mild annoyance and a genuinely powerful guardrail. Embrace the pain now; your future self will thank you.
  • noImplicitAny: Part of strict: true. If enabled, TypeScript will complain if it can’t infer a type for a variable and you haven’t explicitly given it any. This forces you to be explicit and avoid unintentional any types (which defeat the purpose of TypeScript).
    • Example (with noImplicitAny: true):
      • function add(a, b) { return a + b; }Error: Parameter ‘a’ implicitly has an ‘any’ type.
      • let myVariable;Error: Variable ‘myVariable’ implicitly has an ‘any’ type.
    • Solution: function add(a: number, b: number): number { return a + b; } or let myVariable: string;
    • Why this matters: Without explicit types, you lose all the benefits TypeScript provides. The any type is essentially an escape hatch back to JavaScript’s wild west behavior.
  • esModuleInterop: Set this to true. It helps with interoperability between CommonJS and ES Modules, particularly for default imports. It basically adds some compatibility shims during compilation so you can use import React from 'react' even if react library exports using CommonJS module.exports. Without it, you might have to use import * as React from 'react'.
  • skipLibCheck: Set this to true (uncomment it). This makes the compiler skip type checking declaration files (.d.ts) from your node_modules. This speeds up compilation and prevents type errors from third-party libraries, which you usually can’t fix anyway.

Your First TypeScript File

Create a src directory in your project root, and inside it, create index.ts.

src/index.ts:

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

greet("Sanchxt", new Date());

// Try to introduce a JS-style error that TS will catch
// greet("Alice", "Monday"); // This will cause a TypeScript error!
// Argument of type 'string' is not assignable to parameter of type 'Date'.

Now, compile it:

# From your project root (my-ts-project)
npx tsc

You should now have a dist directory with index.js inside it. If you uncommented the error line, tsc would have screamed at you during compilation and probably not generated the dist folder. This is TS doing its job.

dist/index.js (compiled output):

function greet(person, date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Sanchxt", new Date());
// Try to introduce a JS-style error that TS will catch
// greet("Alice", "Monday"); // This will cause a TypeScript error!
// Argument of type 'string' is not assignable to parameter of type 'Date'.

Notice how the types (: string, : Date) are gone? That’s because JS doesn’t understand them. The tsc compiler stripped them out and only left valid JS code.


Development Workflow

Now that you have TypeScript set up, let’s understand what your typical development workflow looks like:

The Watch Mode

Instead of manually running npx tsc every time you make changes, use watch mode:

# This will automatically recompile whenever you save a .ts file
npx tsc --watch

This monitors your TS files and recompiles them automatically whenever you save changes.

IDE Integration

TS truly shines when paired with a good IDE. VS Code has good enough TS support out of the box:

  • Real-time error highlighting: Red squiggly lines appear immediately when you write invalid code
  • IntelliSense: Intelligent code completion that actually knows what properties an object has
  • Refactoring support: Rename a variable across your entire codebase with confidence
  • Go to definition: Click on a function call and instantly jump to where it’s defined

The Transpilation Process

When you run tsc, here’s what happens behind the scenes:

  1. Type Checking: TS analyzes your code for type errors
  2. Syntax Transformation: TS-specific syntax (like type annotations) is removed
  3. Target Compilation: The code is converted to your target JS version
  4. Output Generation: Clean JS files are written to your output directory

This process ensures that by the time your code reaches production, it’s just regular JS that any browser or Node.js runtime can execute.

Before and After

Let’s see a real-world example of how TS prevents common JavaScript pitfalls:

JS (the bad):

function calculateTotal(items, taxRate) {
  return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

// This will blow up at runtime
calculateTotal("not an array", "not a number");
// TypeError: items.reduce is not a function

TS (the decent):

interface Item {
  price: number;
  name: string;
}

function calculateTotal(items: Item[], taxRate: number): number {
  return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

// TypeScript catches this error at compile time:
// calculateTotal("not an array", "not a number");
// Error: Argument of type 'string' is not assignable to parameter of type 'Item[]'

// Correct usage:
const myItems: Item[] = [
  { name: "Coffee", price: 4.5 },
  { name: "Sandwich", price: 8.99 },
];
const total = calculateTotal(myItems, 0.08); // $14.61

Notice how TS not only prevents the error but also makes the code self-documenting. Anyone reading this function immediately knows what kind of data it expects and what it returns.


Takeaways

You’ve now set up a complete TypeScript development environment and understand the fundamental workflow. TS isn’t just about adding type annotations. It’s about fundamentally changing how you think about code reliability, maintainability, and developer experience. It can be the difference between hoping your code works and knowing it works.

This is just Part 1 of a comprehensive TypeScript deep-dive series of articles. I’ll dive deeper in the upcoming parts, but don’t wait for the next article to start practicing. Create some .ts files, experiment with the examples we’ve covered, and get comfortable with the compilation process. The more you use TS, the more you’ll appreciate what you’ve been missing in plain JS.

Ready to never trust a variable again? We’ll do it one type at a time.

See you in the next part, learner :)