Getting Started with TypeScript (for Experienced Python/PHP/JS Developers)

Welcome to this 60-minute TypeScript tutorial. This guide is designed for experienced developers (familiar with Python, PHP, JavaScript, and general web development) who are new to TypeScript, Node.js/npm, React, and related tools. We’ll build a simple full-stack project (Node.js API + React frontend) using TypeScript, all running in Docker containers with Docker Compose for easy setup.

Each section below includes hands-on examples with small code snippets. You can run these examples in a Docker environment (using Dockerfiles and Docker Compose configurations provided). Short, step-by-step instructions and code comments will help you follow along. Let’s dive in!

1. Introduction to TypeScript

What is TypeScript and why use it?
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript (TypeScript in Visual Studio Code). In other words, it’s just JavaScript with optional static typing and some new language features. All valid JavaScript code is also valid TypeScript, but TypeScript adds syntax for type annotations, interfaces, enums, generics, and more (TypeScript vs. JavaScript: Explaining the differences | Contentful). The primary benefit is catching errors during development instead of at runtime (TypeScript vs. JavaScript: Explaining the differences | Contentful) (TypeScript vs. JavaScript: Explaining the differences | Contentful). With TypeScript you can enforce data types, better communicate code intent, and enjoy enhanced IDE support and tooling (TypeScript vs. JavaScript: Explaining the differences | Contentful). These features help you write more robust and maintainable code, especially in large projects.

Key differences from JavaScript: Unlike vanilla JS, TypeScript code must be compiled (transpiled) to JS before it runs. This compilation (usually done with the tsc TypeScript compiler) will check types and report errors if, for example, you pass a string where a number is expected. JavaScript is dynamically typed (type errors surface only when the code is executed), whereas TypeScript is statically typed (type errors are caught at compile time) (TypeScript vs. JavaScript: Explaining the differences | Contentful) (TypeScript vs. JavaScript: Explaining the differences | Contentful). TypeScript also introduces language features not in standard JS, such as interfaces, generics, and access modifiers, to facilitate large-scale application development (TypeScript vs. JavaScript: Explaining the differences | Contentful).

Example – Static typing vs dynamic typing: In JavaScript you might accidentally do this without immediate error:

// JavaScript example (dynamic typing)
let age = 25;
age = "twenty five";  // allowed in JS, but this will cause problems at runtime if used as a number
console.log(age * 2); // NaN at runtime (no compile error)

In TypeScript, you can catch such issues early by declaring types:

// TypeScript example (static typing)
let age: number = 25;
age = "twenty five";  // compile-time error: Type 'string' is not assignable to type 'number'

(TypeScript vs. JavaScript: Explaining the differences | Contentful)The TypeScript compiler (and your editor) will immediately flag the line above as an error. For instance, in VS Code you’d see an error underline with a message like “Type 'string' is not assignable to type 'number'.” This prevents runtime issues. Below, you can see how VS Code highlights this error: (TypeScript vs. JavaScript: Explaining the differences | Contentful)

In the image, TypeScript catches the type mismatch (string vs number) during development, which would save you from a potential bug.

Setting up a TypeScript project with npm: Let’s create a basic TypeScript project. You’ll need Node.js and npm (Node’s package manager) installed. If you’re using Docker for everything, we will set that up shortly – but it’s still useful to understand the project structure.

  1. Initialize npm: Create a new folder for your project (e.g., typescript-demo) and open a terminal there. Run npm init -y. This creates a package.json with default values. (The -y flag skips the questionnaire and uses defaults.)

  2. Install TypeScript compiler: Run npm install --save-dev typescript. This installs TypeScript as a development dependency (since it’s needed to compile your code, but not at runtime). You can install it globally with npm install -g typescript, but using a local dev dependency is common in projects.

  3. Initialize TypeScript configuration: Run npx tsc --init. The tsc command is the TypeScript compiler. Using npx (which comes with npm 5.2+ (Difference between npx and npm? - javascript - Stack Overflow)) executes the local tsc binary without globally installing it. This command creates a default tsconfig.json file in your project.

Configuring tsconfig.json: The tsconfig.json file tells the TypeScript compiler how to compile your project. It marks the root of your TypeScript project and specifies important options (TypeScript: Documentation - What is a tsconfig.json). Open tsconfig.json in an editor – you’ll see a lot of commented options. For now, ensure it has at least these options in "compilerOptions":

{
  "compilerOptions": {
    "target": "ES6",           // which JS version to compile to (ES6 = ECMAScript 2015)
    "module": "commonjs",      // module system (commonjs for Node.js, or "ESNext" for ESM)
    "outDir": "dist",          // output directory for compiled JS files
    "rootDir": "src",          // root directory of your TS source files
    "strict": true,            // enable all strict type-checking options
    "esModuleInterop": true    // allow compatibility imports (for CommonJS default exports)
  },
  "include": ["src"]
}

These settings say: “Take all .ts files in the src folder, and compile them to JavaScript in a dist folder.” You can tweak many options in tsconfig.json to suit your needs; the above is a reasonable starting point. (For example, "strict": true enables TypeScript’s strict type-checking mode which is recommended for new projects.)

Project structure: Create a src directory in your project. All your TypeScript code will go here. For our demo, create a simple file src/index.ts:

// src/index.ts
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const user = "World";
console.log( greet(user) );

This defines a greet function that expects a string and returns a string, then uses it. Notice the type annotation : string for the function parameter and return type.

Compile and run: To compile the project, run npx tsc. This will read tsconfig.json and compile src/index.ts to dist/index.js. You should now have a dist folder with an index.js file. Run it with Node: node dist/index.js. It should print “Hello, World!” in the console. Congratulations – you’ve written and executed your first TypeScript code!

(If you prefer not to compile to a file for every run, you can use npx ts-node src/index.ts to execute TypeScript directly. ts-node is a tool that combines compiling + running in one step. We’ll talk more about such tools later.)

2. Running TypeScript in Node.js (Backend)

Now that we covered the basics of TypeScript, let’s apply it to a Node.js environment by building a simple backend service. We’ll also introduce Docker here to containerize the Node app.

Setting up a Node.js + TypeScript project (with Docker)

For organization, create a new directory (in the root of your tutorial folder) for the backend code, e.g., backend/. Move your typescript-demo files into this backend folder (or create a new Node project inside backend similarly with npm init -y and tsc --init). The final structure might look like:

backend/
  package.json
  package-lock.json
  tsconfig.json
  src/
    index.ts
    util.ts   (optional, for example module)

Inside backend/package.json, add a script to build and run the app. For example:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "4.x"
  }
}

(The exact version of TypeScript may differ. Also, we will add more dev dependencies later for hot-reload, but we’ll get to that.)

We’ll write a slightly more complex example to demonstrate TypeScript features like modules, interfaces, and generics on the backend.

Example backend code (TypeScript):

Create backend/src/util.ts:

// backend/src/util.ts
export interface User {
  id: number;
  name: string;
}

// A generic function example (returns the argument as is, with type preserved)
export function identity<T>(value: T): T {
  return value;
}

// A simple add function
export function add(a: number, b: number): number {
  return a + b;
}

And modify backend/src/index.ts:

// backend/src/index.ts
import { User, identity, add } from "./util";

// Use the imported functions and interface:
const sampleUser: User = { id: 1, name: "Alice" };

console.log(`User: ${sampleUser.name} (id: ${sampleUser.id})`);

// Using the generic identity function
const output = identity<string>("TypeScript is great!");
console.log(output);

// Using the add function
console.log(`2 + 3 = ${add(2, 3)}`);

// Intentionally cause a type error (uncomment the next line to see TypeScript catch it):
// sampleUser.id = "not a number";  // Error: Type 'string' is not assignable to type 'number'

In this code: - We defined an interface User with two fields. In index.ts, we create an object sampleUser of type User. If we try to add a field that isn’t in the interface or use a wrong type, TypeScript will error. - We defined a generic function identity<T> that simply returns whatever is passed in. We explicitly call it with <string> to indicate we expect a string, although TypeScript can infer it. Generics allow writing functions that work with any type while still enforcing type safety. - We have a simple add function to demonstrate importing and using a module. - There’s a commented-out line showing a type error example (sampleUser.id = "not a number"; would be caught by the compiler).

Go ahead and run npm run build (which runs tsc) in the backend directory. Fix any mistakes if the compiler complains. Then run npm start to execute the compiled code. You should see console output similar to:

User: Alice (id: 1)
TypeScript is great!
2 + 3 = 5

Try toggling the commented error line and recompiling to see TypeScript in action.

Dockerizing the Node.js TypeScript App

Now, let’s containerize this backend using Docker. We’ll write a Dockerfile to define how to build a Docker image for our Node.js TypeScript app. A Dockerfile is essentially a script with instructions to assemble a container image (including installing dependencies, compiling the app, etc.) (How To Dockerize a Typescript Node.js App. | by Rainer Regan | Medium).

Create a file backend/Dockerfile with the following content:

# Use an official Node.js runtime as the base image
FROM node:18-alpine

# Set working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json first (for dependency installation)
COPY package*.json ./

# Install dependencies (this will install TypeScript and any other deps)
RUN npm install

# Copy the rest of the application source code
COPY . .

# Build the TypeScript code
RUN npm run build

# Expose a port (if our app was a server, e.g., Express on port 4000, we would expose 4000)
# EXPOSE 4000

# Command to run the app (using the compiled JavaScript)
CMD ["npm", "run", "start"]

Let’s break down what this Dockerfile does:

Now you can build and run the Docker image: ```bash

From the root of the 'backend' directory:

docker build -t ts-backend:dev . docker run --rm ts-backend:dev `` The--rm` flag just removes the container after it exits (since our current script just runs and exits after printing messages). You should see the same output as before, but now running inside a Docker container.

Note: If you had a long-running server (like an Express API), you would run the container in detached mode -d and probably map a port with -p 4000:4000 so you can access it. We’ll cover that soon when we introduce a web server.

At this point, we have a Node.js project written in TypeScript, and we know how to compile and run it both locally and in Docker. Next, we’ll create a React frontend with TypeScript.

3. Using TypeScript with React (Frontend)

Now let’s set up a frontend using React and TypeScript. The popular way to bootstrap a React project is using Create React App (CRA), which has a template for TypeScript.

Setting up a React + TypeScript project

We will create a new React app inside a directory (e.g., frontend/). You can use npx to do this easily:

# From the root of your project (where docker-compose.yml will eventually reside):
npx create-react-app frontend --template typescript

This uses CRA to create a React project named “frontend” and configures it for TypeScript (Getting Started | Create React App). It will generate a new frontend/ folder with a typical React structure and a bunch of files. Key points about this generated project:

Before we dive into code, let’s Dockerize the React app as well, so we can run it in a container.

Dockerizing the React app: Create a frontend/Dockerfile:

FROM node:18-alpine

WORKDIR /app

# Copy react app package files and install dependencies
COPY package*.json ./
RUN npm install

# Copy all source code
COPY . .

# Build the React app for production
RUN npm run build

# Install a simple HTTP server to serve the static files
RUN npm install -g serve

# Expose port 3000 and define the command to run the app
EXPOSE 3000
CMD ["serve", "-s", "build", "-l", "3000"]

This Dockerfile: - Installs dependencies (which include react-scripts, etc.). - Runs npm run build to create a production build (output in the build/ directory). - Installs the serve package globally to serve the static files. - When the container runs, it serves the React app on port 3000.

You can build and run this image similarly: bash docker build -t ts-frontend:dev . docker run -p 3000:3000 --rm ts-frontend:dev After building, access http://localhost:3000 in your browser – you should see the default CRA “spinning logo” app.

However, for development purposes, we usually don’t build the static files each time; we run the dev server. We will configure this in Docker Compose soon, allowing hot-reload. But first, let’s write some TypeScript code in our React app.

Adding TypeScript types to React components and props

Open the CRA project in your editor (the frontend/ folder). The main file is src/App.tsx. By default it’s a functional component with some placeholder content. We’ll modify this to demonstrate using props and state with TypeScript.

In React (with TypeScript), you typically define an interface or type for your component’s props, and then use that with a functional component. Here’s a simple example of a new component that takes props:

// frontend/src/Hello.tsx
import React from 'react';

// Define an interface for the props
interface HelloProps {
  name: string;
  enthusiasmLevel?: number;  // "?" means this prop is optional
}

// A React Functional Component written in TypeScript
const Hello: React.FC<HelloProps> = ({ name, enthusiasmLevel = 1 }) => {
  const greeting = `Hello, ${name}${"!".repeat(enthusiasmLevel)}`;
  return <h2>{greeting}</h2>;
};

export default Hello;

In this Hello component: - We declare HelloProps with a required name (string) and an optional enthusiasmLevel (number, default will be 1). - We use React.FC<HelloProps> to type the functional component. This ensures the component can only be used with the correct props. - Inside the component, we use the props just like normal. If someone tries to use <Hello name={123} /> or forgets the name prop, TypeScript will error.

Now, modify your App.tsx to use this new component:

// frontend/src/App.tsx
import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <div>
      <h1>Welcome to my TypeScript React App</h1>
      <Hello name="TypeScript" enthusiasmLevel={3} />
    </div>
  );
}

export default App;

Run the app (npm start or via Docker Compose as we’ll do soon) and you should see “Hello, TypeScript!!!” on the page (3 exclamation marks because enthusiasmLevel 3).

State management in TypeScript React components: State in functional components is usually handled with React hooks like useState and useEffect. TypeScript works with these hooks by inferring types from initial values, or you can explicitly type them. For example, inside App we could add:

// Example of using useState with TypeScript
import React, { useState, useEffect } from 'react';

function App() {
  const [counter, setCounter] = useState<number>(0);  // state type is number

  useEffect(() => {
    const timer = setInterval(() => setCounter(c => c + 1), 1000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div>
      <h1>Counter: {counter}</h1>
      <Hello name="TypeScript" enthusiasmLevel={3} />
    </div>
  );
}

Here we used useState<number>(0) – by providing the <number> generic, we ensure counter is a number. (TS could infer it from the initial 0, but this is how you’d specify type if needed.) The counter will increment every second, just as a quick demo of state.

The key takeaway is: TypeScript integrates smoothly with React. You describe prop types with an interface or type, and state and effects just work with type inference. The React + TypeScript template has type definitions (@types/react) installed, so things like React.FC and useState are already typed.

Building and running the React project in Docker

We’ve already created a Dockerfile for the React app to build it for production. If you build that image and run it, you get a container serving the app (good for testing production). However, during development, you’d use the npm start dev server because it auto-reloads on file changes. We can use Docker Compose to facilitate this, which we’ll do in the next section.

For now, ensure that both the backend and frontend projects are working: - The backend compiles and runs (printing messages). - The frontend dev server runs (npm start in frontend should open a browser to the app).

Next, we’ll look at some useful npm/npx commands and TypeScript tools, and then combine everything with Docker Compose.

4. Working with npm, npx, and TypeScript Tools

By now, you’ve already used npm (to install packages and run scripts) and npx (to execute create-react-app and tsc). Let’s clarify these and mention some additional tools that make TypeScript development easier.

To summarize, here are a few commands and tools you might use in a TypeScript project: - npm install <pkg> to add libraries. - npm install --save-dev <pkg> to add dev tools (like typescript, ts-node, nodemon, testing libs, etc.). - npx tsc --init to initialize a tsconfig. - npm run build (with a script) to compile. - npm start or npm run dev to run the app (perhaps via ts-node or after building). - npx nodemon --watch src --exec "ts-node src/index.ts" to auto-restart a TS Node app on changes (you can put this in a script).

Next, we will set up Docker Compose to orchestrate running both our backend and frontend together, with hot-reloading enabled for both.

5. Docker Compose Setup

Docker Compose allows us to define and run multi-container applications with a single YAML file. We have two services: the Node.js API (backend) and the React app (frontend). We want to run them together easily, and also enable a good development workflow (editing code on our host machine, and seeing changes live in the containers).

Create a docker-compose.yml file at the root of your project (above the backend/ and frontend/ directories):

version: "3.8"

services:
  api:
    build: ./backend
    ports:
      - "4000:4000"
    volumes:
      - ./backend:/app
      - /app/node_modules
    command: npm run dev

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    command: npm start

Let’s unpack this Compose file: - We define two services: api and frontend. - Each service has a build context (pointing to the respective directory). This means Docker Compose will use the Dockerfile in that directory to build the image. (The first time, or whenever Dockerfile changes or you run with --build.) - ports: We map the container ports to our host. The API will listen on 4000 inside the container, mapped to localhost:4000. The React dev server runs on 3000 inside, mapped to localhost:3000. - volumes: This is important for development. We mount our code into the container. ./backend:/app means “take my local backend folder and mount it at /app inside the container, overwriting whatever was built into the image.” This way, as we edit code locally, it instantly reflects inside the container. We also add a second volume /app/node_modules – this is a trick to prevent the host from overwriting the node_modules in the container. We want to use the container’s installed modules, not our host’s (which for many, especially Windows/macOS, might not even exist or be incompatible). - command: We override the default command (which was set to build or serve in our Dockerfiles) to instead run the dev mode. For the API, we’ll create an npm run dev script to start the server in watch mode. For the frontend, npm start is the CRA dev server.

This setup allows us to run docker-compose up and get: - A backend container running our Node API, restarting on file changes. - A frontend container running the CRA dev server, with hot-reload for React code.

Enabling hot-reload for the Node API: We need to implement npm run dev in our backend. Let’s use ts-node-dev for simplicity. Install it in the backend: run npm install --save-dev ts-node-dev. Then in backend/package.json, add:

"scripts": {
  "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
  "build": "tsc",
  "start": "node dist/index.js"
}

Enabling hot-reload for React: CRA’s dev server already supports hot reload out of the box. We’ve mounted the code, so any edits to files in frontend/src will trigger the CRA dev server to recompile and the browser to refresh. One thing: file watching in Docker containers can sometimes be finicky on certain OSes. If you find changes aren’t detected, you can enable polling. For CRA, you can set an env var CHOKIDAR_USEPOLLING=true to force polling. In Compose, that would be:

    environment:
      - CHOKIDAR_USEPOLLING=true

(This can be necessary on Windows/Mac with Docker.) But try without it first.

Now, let’s run the whole stack:

docker-compose up --build

Once up, open http://localhost:3000. You should see the React app (with our Hello component, etc.). The React dev server proxies API calls if configured, but we haven’t set that. If you want the React app to talk to the API, you have two main options: 1. In development, you can call the API at http://localhost:4000 (since we exposed it). However, due to CORS (Cross-Origin Resource Sharing), the browser will block requests from 3000 to 4000 unless the API allows it. 2. Use CRA’s proxy feature: add "proxy": "http://localhost:4000", in frontend/package.json. CRA will then proxy unknown requests (like API calls) to the API, avoiding CORS in dev.

For now, let’s handle CORS in the API for simplicity (so we can test it easily). We will address this in the final project.

The Compose setup above runs both services. If you edit a backend .ts file, you should see the backend container logs show a restart (thanks to ts-node-dev). If you edit a React file, the frontend container logs will show a rebuild and your browser will auto-refresh. This gives a tight feedback loop, all within Docker.

You can stop the services with Ctrl+C. To remove containers (and networks) run docker-compose down.

We now have a full development environment in Docker Compose!

6. VS Code Setup

Finally, let’s ensure our development environment (VS Code) is optimized for TypeScript, Node, and React.

Recommended VS Code extensions: - TypeScript and JavaScript Language Features: This comes built-in with VS Code (it provides IntelliSense, autocompletion, and error highlighting for JS/TS). Just make sure it’s enabled (it should be by default). - ESLint: If you plan to use a linter (recommended), install the official ESLint extension. It will highlight code style issues and errors as you type. (CRA includes a basic ESLint config; you’ll see red squiggly underlines for things like unused variables.) After installing, you might need to enable it in VS Code settings. (How To Lint and Format Code with ESLint in Visual Studio Code | DigitalOcean)To install, open the Extensions pane in VS Code, search “ESLint”, and click Install. An example of the ESLint extension page in VS Code is shown below. (How To Lint and Format Code with ESLint in Visual Studio Code | DigitalOcean)

The image above shows the ESLint extension in VS Code. After installing, ESLint will underline problems in your code according to your configuration.

Debugging TypeScript in VS Code: - Node.js Debugging: If you want to debug the Node API, you have a couple of options. One easy way during development: run the API with the inspector enabled. For example, adjust the Docker compose command for api to: node --inspect=0.0.0.0:9229 dist/index.js (if running compiled code) or in ts-node-dev you can pass --inspect flag. Then, map port 9229 in docker-compose (e.g., - "9229:9229"). In VS Code, use the “Attach to Node.js” debug configuration, which by default looks for a process listening on 9229. This will attach the debugger to the Node process inside the container. You can then set breakpoints in your TypeScript code. Thanks to source maps, VS Code will stop at the correct lines in your TS files (TypeScript tutorial in Visual Studio Code). Ensure "sourceMap": true and "outDir": "dist" are in tsconfig so VS Code can map to your sources. - Alternatively, you can debug locally by running your Node app on your machine (outside Docker) with VS Code’s debugger. But if you want the full Docker environment, the remote attach method works as described. - React Debugging: For React, most debugging is done through the browser. You can use React Developer Tools extension in the browser to inspect component state and props. VS Code can debug front-end code via the Edge/Chrome debugger extensions, but that’s an advanced topic. In practice, console logs or the browser dev tools are sufficient for many cases, especially for a simple project.

VS Code and Docker Compose Integration: VS Code has a “Remote - Containers” extension that can even open your project inside a container for a seamless dev environment. This is optional and beyond our scope, but know that it’s possible to develop “in container” for consistency. For now, our approach is editing locally and running in containers via Compose, which is perfectly fine.

Make sure in VS Code you open the root folder of the project (so it sees both backend and frontend). The TypeScript language server will pick up your tsconfig.json files and provide IntelliSense and error checking. When you open a .ts or .tsx file, you should see any type errors underlined in red. You can hover to see the error message. This tight integration helps catch errors even before running the compiler manually.

7. Hands-on Demos

Throughout this tutorial, we’ve included hands-on examples. Let’s recap and ensure you try them:

These small demos verify that our Docker + TypeScript setup is working for each part. Each concept we discussed (type checking, modules, React props, etc.) was accompanied by sample code – make sure to run those and observe the behavior. Nothing beats seeing a TypeScript error for yourself or a successful hot-reload after saving a file!

8. Final Challenge: Build a Simple Full-Stack App (API + Frontend)

Now it’s time to combine everything you’ve learned in a mini project. The goal is to create a simple API endpoint in the Node backend and consume it from the React frontend. We’ll make a minimal example: the API will have one route that returns a greeting message, and the React app will fetch that message and display it.

Step 1: Extend the Backend (Node API)
We’ll use Express (a popular web framework for Node) to set up a quick API. Install Express and its type definitions in the backend:

# in the backend directory
npm install express
npm install --save-dev @types/express

Update backend/src/index.ts to create a basic Express server:

// backend/src/index.ts
import express, { Request, Response } from 'express';
import cors from 'cors';  // we'll allow CORS for simplicity

const app = express();
const PORT = process.env.PORT || 4000;

// Use CORS middleware to allow requests from our frontend
app.use(cors());

// Define a Type for our API response (for TypeScript usage)
interface MessageResponse {
  message: string;
}

// API route: GET /api/hello
app.get('/api/hello', (req: Request, res: Response<MessageResponse>) => {
  res.json({ message: "Hello from TypeScript Node API" });
});

// Start the server
app.listen(PORT, () => {
  console.log(`✅ API is running at http://localhost:${PORT} (inside Docker: ${PORT})`);
});

Comments explaining the code: - We import express and also import Request and Response types for typing our route handler. - We use cors() to allow cross-origin requests (so our React dev server at localhost:3000 can call this API at localhost:4000). In a real app, you’d configure CORS more restrictively. - We define an interface MessageResponse to type the JSON we’ll return. - We set up a GET route at /api/hello that responds with a JSON object like { message: "Hello from TypeScript Node API" }. We annotate the res: Response<MessageResponse> to indicate the response body structure. - Finally, we start the server on port 4000 (the Docker container will use this, and we mapped it to host 4000 in the compose file). We log a message when it’s up (prefixed with ✅ just to stand out).

Update backend/package.json to have the updated scripts if needed and ensure ts-node-dev is installed (we did that earlier). Our npm run dev will now run this server and keep running (no longer an exit-after-print program).

Step 2: Extend the Frontend (React app)
In the React app, we’ll fetch from this API. We can do this in App.tsx directly for this simple case.

Open frontend/src/App.tsx and modify it:

import React, { useState, useEffect } from 'react';

interface MessageResponse {
  message: string;
}

function App() {
  const [apiMessage, setApiMessage] = useState<string>("(Loading...)");

  useEffect(() => {
    // Fetch the API message on component mount
    fetch('http://localhost:4000/api/hello')
      .then(res => res.json())
      .then((data: MessageResponse) => {
        setApiMessage(data.message);
      })
      .catch(err => {
        console.error("Error fetching message:", err);
        setApiMessage("(Failed to load)");
      });
  }, []);  // empty dependency array -> runs once on mount

  return (
    <div style={{ fontFamily: 'sans-serif', textAlign: 'center', marginTop: '2rem' }}>
      <h1>My TypeScript Full-Stack App</h1>
      <p><strong>Message from API:</strong> {apiMessage}</p>
    </div>
  );
}

export default App;

Let’s explain: - We define a MessageResponse interface to match the API response shape (so TypeScript knows data.message is a string). - We use React’s state hook to keep apiMessage. It starts as "(Loading...)". - In a useEffect with empty deps (so it runs only on initial render), we call fetch('http://localhost:4000/api/hello'). When the promise resolves, we parse JSON and then cast the data to MessageResponse type (or you could use data as MessageResponse). We then set the message in state. - If there’s an error (e.g., network issue), we catch it and set a failure message. - The UI just displays a heading and the message from the API (or the loading/failure text).

Step 3: Update Docker Compose (if needed)
Our docker-compose.yml already exposes port 4000 for the API and 3000 for the frontend. We added Express and maybe other dependencies, so we should rebuild: bash docker-compose up --build This will rebuild the backend image (installing express) and start both services.

Step 4: Test the integrated app
- Go to http://localhost:3000 in your browser. You should see “My TypeScript Full-Stack App” and below that, “Message from API: Hello from TypeScript Node API”. The message came from the backend! - You can also directly visit http://localhost:4000/api/hello in the browser; you should see the raw JSON: {"message":"Hello from TypeScript Node API"}. - Try editing the backend message (change the string in res.json({...}) to something else), and watch the Docker logs: the backend should restart (ts-node-dev) and the frontend might show the old message until you refresh. Refresh the browser to see the new message from the API. - You can also edit the frontend (e.g., change the text in the <h1> tag). The browser should live-update (CRA will refresh the page or inject changes).

9. Creating an Executable NPX App with TypeScript

In this chapter, we will learn how to create an executable npx app that runs TypeScript files directly. This allows us to distribute a command-line tool that users can run using npx <your-app>, without needing to install it globally.


Step 1: Initialize a Node.js Package

To create an npx-runnable app, we need to set up an npm package.

mkdir my-npx-app && cd my-npx-app
npm init -y

This creates a package.json file with default settings.


Step 2: Create the TypeScript Executable File

Inside your project, create a bin/ directory and an index.ts file:

mkdir bin
touch bin/index.ts

Now, open bin/index.ts and add the following code:

#!/usr/bin/env ts-node

console.log("Hello from My NPX App!");

Explanation


Step 3: Update package.json

Modify your package.json to include a bin field, which defines the command users will run:

{
  "name": "my-npx-app",
  "version": "1.0.0",
  "bin": {
    "my-npx-app": "bin/index.ts"
  },
  "dependencies": {},
  "devDependencies": {
    "typescript": "^4.0.0",
    "ts-node": "^10.0.0"
  }
}

What This Does


Step 4: Install Dependencies

Now, install the required dependencies:

npm install

Step 5: Make the File Executable

To allow the system to run this script as a command, set its execution permissions:

chmod +x bin/index.ts

Step 6: Test the NPX App Locally

Before publishing, test your executable without installing it globally.

Run the command: bash npx ./bin/index.ts You should see: Hello from My NPX App!

Alternatively, you can test it as a global package: bash npm link # Creates a global symlink my-npx-app # Runs the command This makes my-npx-app available globally on your machine.


Step 7: Publish to npm (Optional)

If you want others to use your npx app, publish it to npm:

npm publish --access public

Once published, anyone can run your tool with: bash npx my-npx-app


Conclusion

You’ve now created an executable TypeScript-based NPX app! This method allows you to: - Distribute CLI tools easily via npx. - Execute TypeScript files directly without compiling them first. - Run commands without global installation (perfect for one-time utilities).

This is how many CLI tools (like create-react-app, ts-node, and eslint) are built. Now, you can create and distribute your own npx-powered TypeScript applications! 🚀

10. TypeScript Cheat Sheet – Quick Reference

This TypeScript cheat sheet summarizes the key syntax, types, and features you need to know, with concise code snippets and explanations. Use this as a quick reference while coding in TypeScript.


1. Basic Types

TypeScript provides strict type checking for common JavaScript types.

let num: number = 42;
let str: string = "Hello";
let isDone: boolean = true;
let arr: number[] = [1, 2, 3];  // Array of numbers
let mixedArr: (number | string)[] = [1, "two", 3];  // Array with mixed types

2. Type Inference

TypeScript can automatically infer types.

let message = "Hello, TypeScript";  // Inferred as string
// message = 10;  // Error: Type 'number' is not assignable to type 'string'

3. Functions

TypeScript allows function type annotations.

function add(a: number, b: number): number {
  return a + b;
}

const greet = (name: string): string => `Hello, ${name}`;

4. Optional and Default Parameters

Use ? for optional parameters, and = for default values.

function greet(name: string, age?: number): string {
  return age ? `${name} is ${age} years old` : `Hello, ${name}`;
}

function multiply(a: number, b: number = 10): number {
  return a * b;
}

console.log(greet("Alice"));  // Hello, Alice
console.log(greet("Bob", 25));  // Bob is 25 years old
console.log(multiply(5));  // 50 (default b = 10)

5. Interfaces

Interfaces define object structure and enforce type safety.

interface Person {
  name: string;
  age: number;
  isStudent?: boolean;  // Optional property
}

const user: Person = { name: "Alice", age: 25 };

6. Type Aliases

Type aliases work similarly to interfaces but are more flexible.

type Point = { x: number; y: number };

const point: Point = { x: 10, y: 20 };

7. Readonly and Const

Prevent modification using readonly or const.

interface Car {
  readonly brand: string;
  model: string;
}

const myCar: Car = { brand: "Toyota", model: "Camry" };
// myCar.brand = "Honda";  // Error: brand is readonly

const numbers: readonly number[] = [1, 2, 3];
// numbers.push(4);  // Error: Readonly array

8. Enums

Enums define a set of named constants.

enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500,
}

const response: Status = Status.Success;
console.log(response);  // 200

9. Tuples

Tuples define fixed-length arrays with specific types.

let tuple: [string, number] = ["Alice", 30];
// tuple = [30, "Alice"];  // Error: Order must match

10. Union and Intersection Types

// Union type
let id: number | string;
id = 123;
id = "ABC123";

// Intersection type
type Employee = { name: string };
type Manager = { department: string };

type Boss = Employee & Manager;
const boss: Boss = { name: "Alice", department: "HR" };

11. Type Assertions (as and <type>)

Force TypeScript to treat a value as a specific type.

let someValue: unknown = "Hello TypeScript";
let strLength: number = (someValue as string).length;
// OR
let strLength2: number = (<string>someValue).length;

12. Generics

Generics allow functions to work with multiple types.

function identity<T>(value: T): T {
  return value;
}

console.log(identity<string>("Hello"));  // Hello
console.log(identity<number>(42));  // 42

13. Classes in TypeScript

TypeScript supports OOP concepts like classes, interfaces, and inheritance.

class Animal {
  constructor(public name: string) {}

  makeSound(): void {
    console.log("Animal makes a sound");
  }
}

class Dog extends Animal {
  bark(): void {
    console.log("Woof! Woof!");
  }
}

const myDog = new Dog("Buddy");
myDog.makeSound();  // Animal makes a sound
myDog.bark();  // Woof! Woof!

14. Access Modifiers (public, private, protected)

Control class property accessibility.

class Person {
  public name: string;  // Accessible anywhere
  private age: number;  // Accessible only within the class
  protected email: string;  // Accessible in subclasses

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

class Employee extends Person {
  showEmail(): string {
    return this.email;  // Works because email is protected
  }
}

const emp = new Employee("Alice", 30, "alice@example.com");
// emp.age = 30;  // Error: Property 'age' is private

15. TypeScript Modules (import / export)

Break code into multiple files using modules.

// mathUtils.ts (exporting)
export function add(a: number, b: number): number {
  return a + b;
}

// main.ts (importing)
import { add } from "./mathUtils";

console.log(add(2, 3));  // 5

16. Working with Promises and Async/Await

TypeScript provides full support for async programming.

function fetchData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Data received"), 1000);
  });
}

// Using Promises
fetchData().then(console.log);

// Using Async/Await
async function getData() {
  const data = await fetchData();
  console.log(data);
}
getData();

17. Null, Undefined, and Strict Mode

Enable strict mode to enforce safe handling of null and undefined.

let value: string | null = null;

function printLength(str?: string): void {
  console.log(str?.length ?? "No string provided");
}

printLength("Hello");  // 5
printLength();  // "No string provided"

18. Using typeof and instanceof

Check types at runtime.

let input: unknown = "Hello";
if (typeof input === "string") {
  console.log(input.toUpperCase());
}

class Car {}
const myCar = new Car();
console.log(myCar instanceof Car);  // true

Final Thoughts

This cheat sheet covers the most commonly used TypeScript features. Keep it handy as a quick reference when working with TypeScript.

🚀 Next Steps:
- Try using TypeScript in a real project. - Experiment with generics, modules, and async programming. - Explore advanced types like mapped types, utility types, and discriminated unions.

Happy coding! 🎯

🎉 Congratulations! You’ve built a mini full-stack application using TypeScript on both the server and client, and you’ve run it in Docker containers. In doing so, you learned how to set up a Node.js project with TypeScript, how to create a React app with TypeScript, how to use npm/npx and scripts to manage tasks, and how to use Docker and Docker Compose to streamline development and ensure a consistent environment.


Next Steps: From here, you can expand this project with more routes, more complex data, perhaps a database, or add more React components. TypeScript will scale with you, catching errors as your application grows. You might also explore other frameworks or tools: - Use a more advanced build tool like Webpack or Vite for bundling (Create React App uses Webpack under the hood, but tools like Vite can be even faster for dev). - Explore different TypeScript config options (strict mode flags, path aliases, etc.). - Add testing to your project (Jest for React/TypeScript, for instance). - Learn more about TypeScript’s advanced type system (Union types, intersection types, mapped types, etc.) as you build more complex features.

But even without those, you now have a solid starting point for developing with TypeScript in a modern web stack. Happy coding!