In this chapter we will…
- Get hands-on with our first TypeScript program and the compiler CLI command
- Learn how the compiler-emitted JS code changes depending on JS language level and module type
- Examine a simple program’s compiled output, including the type declaration file
Anatomy of the project
Let’s consider a very simple TypeScript project
that consists of only three files:
shpackage.json # Package manifesttsconfig.json # TypeScript compiler settingssrc/index.ts # "the program"
This project can be found in the packages/welcome-to-ts folder, within the workshop repo
shcd packages/welcome-to-ts
package.json
(view source)
jsonc{"name": "welcome-to-ts","license": "NOLICENSE","devDependencies": {"typescript": "^5.2.0"},"scripts": {"dev": "tsc --watch --preserveWatchOutput"}}
Note that…
- We just have one dependency in our package.json:
typescript. -
We have a
devscript (this is what runs when you invokeyarn dev-welcome-to-tsfrom the project root)- It runs the TypeScript compiler in “watch” mode (watches for source changes, and rebuilds automatically).
The following is just about the simplest possible config file for the TS compiler:
tsconfig.json
(view source)
jsonc{"compilerOptions": {"outDir": "dist", // where to put the TS files"target": "ES2015", // JS language level (as a build target)"moduleResolution": "Node" // Find cjs modules in node_modules},"include": ["src"] // which files to compile}
All of these things could be specified on the command line (e.g., tsc --outDir dist), but particularly as
things get increasingly complicated, we’ll benefit a lot from having this config file:
Finally, we have a relatively simple and pointless TypeScript program. It does
have a few interesting things in it that should make changes to the "target"
property in our tsconfig.json more obvious:
- Use of a built in
Promiseconstructor (introduced in ES2015) - Use of
asyncandawait(introduced in ES2017) - Use of a
staticprivate class field (introduced in ES2022)
Here is the original (TypeScript) source code that we aim to compile:
src/index.ts
(view source)
tsTry/*** Create a promise that resolves after some time* @param n number of milliseconds before promise resolves*/functiontimeout (n : number) {return newPromise ((res ) =>setTimeout (res ,n ))}/*** Add two numbers* @param a first number* @param b second*/export async functionaddNumbers (a : number,b : number) {awaittimeout (500)returna +b }classFoo {static #bar = 3staticgetValue () {returnFoo .#bar}}//== Run the program ==//;(async () => {console .log (awaitaddNumbers (Foo .getValue (), 4))})()
Note that when you hover over certain code points on this website, you get the equivalent of a “VScode tooltip”. This is one of our most important tools for learning about how TypeScript understands our code!

Running the compiler
From within the packages/welcome-to-ts folder of the git repo, you can run
shtsc
to build the project. Alternatively, from within the same folder, you can run this command to start a task that will rebuild the project whenever you change important files.
shyarn dev
You should see something in your terminal like:
12:01:57 PM - Starting compilation in watch mode...
Note that within the “welcome-to-ts” project
- a
./distfolder has appeared, - inside it is an
index.jsfile.
Open this file — it will be a mess
Click here to see what the compiled output looks like
jsTryvar__awaiter = (this && this.__awaiter ) || function (thisArg ,_arguments ,P ,generator ) {functionadopt (value ) { returnvalue instanceofP ?value : newP (function (resolve ) {resolve (value ); }); }return new (P || (P =Promise ))(function (resolve ,reject ) {functionfulfilled (value ) { try {step (generator .next (value )); } catch (e ) {reject (e ); } }functionrejected (value ) { try {step (generator ["throw"](value )); } catch (e ) {reject (e ); } }functionstep (result ) {result .done ?resolve (result .value ) :adopt (result .value ).then (fulfilled ,rejected ); }step ((generator =generator .apply (thisArg ,_arguments || [])).next ());});};var__classPrivateFieldGet = (this && this.__classPrivateFieldGet ) || function (receiver ,state ,kind ,f ) {if (kind === "a" && !f ) throw newTypeError ("Private accessor was defined without a getter");if (typeofstate === "function" ?receiver !==state || !f : !state .has (receiver )) throw newTypeError ("Cannot read private member from an object whose class did not declare it");returnkind === "m" ?f :kind === "a" ?f .call (receiver ) :f ?f .value :state .get (receiver );};var_a ,_Foo_bar ;/*** Create a promise that resolves after some time* @param n number of milliseconds before promise resolves*/functiontimeout (n ) {return newPromise ((res ) =>setTimeout (res ,n ));}/*** Add two numbers* @param a first number* @param b second*/export functionaddNumbers (a ,b ) {return__awaiter (this, void 0, void 0, function* () {yieldtimeout (500);returna +b ;});}classFoo {staticgetValue () { return__classPrivateFieldGet (_a ,_a , "f",_Foo_bar ); }}_a =Foo ;_Foo_bar = {value : 3 };//== Run the program ==//(() =>__awaiter (void 0, void 0, void 0, function* () {console .log (yieldaddNumbers (Foo .getValue (), 4));}))();
If you’re familiar with the very old ES5 JavaScript language level, there are a few things in here that tell us we’re dealing with ES2015. For example, the use of the yield keyword, a generator function*, classes and a Promise constructor.
Changing target language level
If we go to welcome-to-ts/tsconfig.json and change the “compilerOptions.target” property:
diff{"compilerOptions": {"outDir": "dist",- "target": "ES2015"+ "target": "ES2017"},"include": ["src"]}
Look at that dist/index.js file again — it’s much cleaner now! Do you notice what has changed?
Click here to see what the compiled output looks like
tsTryvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);};var _a, _Foo_bar;/*** Create a promise that resolves after some time* @param n number of milliseconds before promise resolves*/function timeout(n) {return new Promise((res) => setTimeout(res, n));}/*** Add two numbers* @param a first number* @param b second*/export async function addNumbers(a, b) {await timeout(500);return a + b;}class Foo {static getValue() {return __classPrivateFieldGet(_a, _a, "f", _Foo_bar);}}_a = Foo;_Foo_bar = { value: 3 };//== Run the program ==//;(async () => {console.log(await addNumbers(Foo.getValue(), 4));})();
Some changes to observe:
- We start to see
asyncandawait - We no longer see the
_awaiterhelper
It’s starting to look more recognizable — as if the type information has just been stripped away from our original .ts source code in some places. There’s still some compiler-provided workarounds for the private class field, which isn’t supported in native JavaScript until ES2022.
And now finally, let’s try ES2022:
diff{"compilerOptions": {"outDir": "dist",- "target": "ES2017"+ "target": "ES2022"},"include": ["src"]}
Click here to see what the compiled output looks like
tsTry/*** Create a promise that resolves after some time* @param n number of milliseconds before promise resolves*/function timeout(n) {return new Promise((res) => setTimeout(res, n));}/*** Add two numbers* @param a first number* @param b second*/export async function addNumbers(a, b) {await timeout(500);return a + b;}class Foo {static #bar = 3;static getValue() {return Foo.#bar;}}//== Run the program ==//;(async () => {console.log(await addNumbers(Foo.getValue(), 4));})();
Declaration Files
You may also notice that a .d.ts file is generated as part of the compile process. This is known as a declaration file.
tsTry/*** Add two numbers* @param a first number* @param b second*/export declare function addNumbers(a: number, b: number): Promise<number>;
A good way to think of TS files:
.tsfiles contain both type information and code that runs.jsfiles contain only code that runs.d.tsfiles contain only type information
There are other types of file extensions which have a TypeScript equivalent
| File Purpose | JS extension | TS extension |
|---|---|---|
| React | .jsx |
.tsx |
| Native ES Modules | .mjs |
.mts |
Types of modules
CommonJS
Did you notice that the export keyword was still present in the build output for our program? We are generating ES2015 modules from our TypeScript source.
If you tried to run this file with node like this:
shnode packages/welcome-to-ts/dist/index.js
There’s an error!
shexport async function addNumbers(a, b) {^^^^^^SyntaxError: Unexpected token 'export'
It seems that, at least with most recent versions of Node.js and the way our project is currently set up, we can’t just run this program directly as-is.
Node conventionally expects CommonJS modules 1, so we’ll have to tell TypeScript to output this kind of code.
Let’s add a new property to our tsconfig file:
diff"compilerOptions": {"outDir": "dist",+ "module": "CommonJS",
Look at your packages/welcome-to-ts/dist/index.js one more time now. You should see
that the way the addNumbers function is exported has changed:
jsTryexports .addNumbers =addNumbers
This is an indication that we’re emitting CommonJS modules! We could try running
this program with node one more time:
shnode packages/welcome-to-ts/dist/index.js
If the program works correctly at this point, we should see it pause for a short
time and then print 7 to the console, before ending successfully.
ES Modules
Node now supports running native modules (.ejs files) directly! We can configure TypeScript to build this type of file. Make this change to your tsconfig.json
diff"target": "ES2022",+ "module": "NodeNext",- "module": "CommonJS",- "moduleResolution": "Node"
Build the file again by running tsc while in the ./packages/welcome-to-ts folder.
Finally, run from within the same folder
shnode dist/index.js
And you should see 7 printed to the console again!
CONGRATS! You’ve just compiled your first TypeScript program!