Function Types and Return Types
Running node example.ts is fine for learning and quick experiments. But for real projects you need proper tooling: a compiler that type-checks and transpiles your code, and a linter that catches style and correctness issues.
This document covers setting up a TypeScript project with the TypeScript compiler (tsc) and optionally adding ESLint.
The TypeScript compiler does two independent things:
These steps are independent. A file with type errors still produces runnable JavaScript — tsc reports the errors but emits the output anyway by default. This surprises many newcomers: type errors are warnings about correctness, not syntax errors that prevent execution. You can change this with the noEmitOnError option in tsconfig.json.
npm init -y
npm install typescript
npx tsc --init
This creates a tsconfig.json file with many options (most commented out). Here's a minimal starting point:
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
<aside> ⚠️
Make sure your package.json includes "type": "module" so Node.js uses ES modules.
</aside>
What these options mean:
| Option | What it does |
|---|---|
target |
Which JavaScript version to output. es2022 is a safe modern default. |
module |
Module system for the output. nodenext for Node.js ES module projects. |
moduleResolution |
How TypeScript finds imported modules. Must match module. |
outDir |
Where compiled .js files go. Keeps source and output separate. |
rootDir |
Where your .ts source files live. |
strict |
Enables all strict type-checking options. Always use this. |
esModuleInterop |
Lets you import express from 'express' instead of import * as express. |
skipLibCheck |
Skips type-checking .d.ts files from node_modules. Speeds up compilation. |
forceConsistentCasingInFileNames |
Prevents issues on case-insensitive file systems. |
my-project/
├── src/
│ └── index.ts
├── dist/ ← generated by tsc, add to .gitignore
├── tsconfig.json
└── package.json
npx tsc # compile all .ts files in src/ → dist/
node dist/index.js # run the compiled JavaScript
npx tsc --watch # recompile automatically on file changes
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch"
}
}
All the tools above do the same fundamental thing: they remove type annotations so that a JavaScript engine can execute the code. The annotations are only used during development — they help your editor and the compiler catch mistakes, but they are not part of the JavaScript language.
Here's a TypeScript function:
function greet(name: string, age: number): string {
return `Hello, ${name}. You are ${age} years old.`;
}
const message: string = greet('Aisha', 27);
After removing the types, you get plain JavaScript:
function greet(name, age) {
return `Hello, ${name}. You are ${age} years old.`;
}
const message = greet('Aisha', 27);
Every : string and : number annotation is gone. The logic is identical — the types were only there to help you write correct code.
You can see this in action in the TypeScript Playground — paste any TypeScript on the left and the compiled JavaScript appears on the right.
How each tool handles this removal differs: tsc parses and type-checks first, then emits JavaScript. tsx uses esbuild to strip types at high speed without checking them. Node.js native support replaces type annotations with whitespace in-place[^1], so line numbers stay the same without needing source maps.
[^1]: This is why node example.ts works even though Node.js only runs JavaScript — by the time the code reaches the JavaScript engine, the types are already gone.
ESLint catches bugs, enforces consistency, and flags patterns that TypeScript's type checker doesn't cover — things like unused variables, unreachable code, or inconsistent naming.
Install ESLint and the TypeScript ESLint plugin:
npm install eslint @eslint/js typescript-eslint
Create an eslint.config.js file in your project root (ESLint v9+ uses flat config):
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['dist/'],
},
);
This gives you a solid set of rules out of the box, including TypeScript-specific rules like catching unused variables, preferring const, and flagging unsafe any usage.
The recommended config is a good starting point. When you're ready for stricter checks, typescript-eslint offers strict and strictTypeChecked configs that catch more issues — like unhandled promises, unsafe type assertions, and unnecessary conditions. See the typescript-eslint docs for details.
Add a script to package.json:
{
"scripts": {
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
}
}
Run it:
npm run lint # report issues
npm run lint:fix # auto-fix what can be fixed
Install the ESLint extension for real-time linting in your editor. Errors and warnings appear as squiggles, just like TypeScript's own type errors.
The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0

Found a mistake or have a suggestion? Let us know in the feedback form.