TypeScript Fundamentals: Getting Started (Part 1/2)

Nowadays JavaScript is everywhere, powering websites, mobile apps, desktop software, and games on both client and server. Its ubiquity has made it an essential tool for developers in nearly every domain, creating a universal programming ecosystem that transcends traditional platform boundaries and specializations.
JavaScript's flexibility is one of its greatest strengths, allowing quick prototyping given its dynamic nature, but it can sometimes be its biggest weakness. Consider this code:
Once deployed, this code could result in your website crashing.

toFixed()
, on an undefined
value.But with TypeScript, the same code fails instantly in your editor:

user.age
on an object that might be undefined
, preventing a potential runtime error.TypeScript catches the mistake as you type, not after deployment.
This is TypeScriptâs superpower: it brings structure to JavaScriptâs flexibility, keeping your sanity intact as projects grow.
But TypeScript wasnât built in a vacuum. Before we dive into code, letâs take a step back and look at why TypeScript was created and what problems it aimed to solve. Knowing the why behind TypeScript makes it easier to learn and apply with confidence.
This is the first part of a two-part series. By the end of these installments, youâll not only be able to write basic TypeScript, but also understand why it exists and how it fits into the JavaScript ecosystem.
Now, without further ado, letâs dive in.
JavaScriptâs Backstory
JavaScript was created in 1995 as a simple scripting language to add interactivity to otherwise static HTML pages. It was originally intended for small snippets of code embedded directly into HTML, and writing more than a few dozen lines was uncommon. At the time, it was never designed for building large-scale applications.
Over time, though, JavaScript became increasingly popular, and web developers started using it to create more complex and interactive experiences. Its versatility led to adoption beyond browsers, with the rise of server-side JavaScript through Node.js, first released in 2009.
Given its original purpose for small-scale scripting, JavaScript was designed as a dynamically typed language, a choice that worked well for its early goals but introduced challenges as applications grew larger.
Statically vs Dynamically Typed Languages
In statically typed languages like C, C++, and Java, you explicitly define variable types. The compiler checks for type-related errors before the program runs, catching mistakes early, like trying to access a property that doesn't exist on an object.
Dynamically typed languages like JavaScript, by contrast, don't enforce types at compile time. Variables can hold any value, and many mistakes go unnoticed until runtime, leading to unexpected errors. We saw an example of this earlier: trying to call .toFixed()
on a missing age
property caused a runtime error that could crash the application.
This kind of flexibility is useful for small scripts and quick iterations. But as codebases grow larger, the lack of type safety makes maintenance harder. Bugs slip through until runtime, and refactoring becomes risky and error-prone.
These growing pains are exactly what TypeScript was created to address. It introduces optional static typing, improves tooling, and makes JavaScript development safer without sacrificing the flexibility that made it so popular in the first place.
The next couple of sections provide valuable context that will make the core concepts easier to understand. However, if youâd prefer to jump straight into the syntax, you can skip ahead to the Basic Concepts section.
TypeScriptâs Arrival
The 2010s saw an explosion of JavaScript frameworks. Tools like AngularJS, Ember.js, and React enabled developers to build increasingly structured and interactive web applications. Combined with the rise of Node.js, JavaScript evolved from a browser-only scripting language into a language powering full-stack development.
But as applications grew more complex, JavaScriptâs dynamic nature became a liability. The lack of type safety made refactoring risky, tooling was limited, and runtime errors often slipped through the cracks.
For example, consider a common refactor:
JavaScript wonât warn you, it assumes you know what youâre doing. A typo or stale property access might only surface in production. With TypeScript, this kind of issue is caught immediately by the type checker, making large-scale changes safer and faster.
Microsoft had already explored solutions to these challenges. In the mid-2000s, they experimented with Script#, a project by Nikhil Kothari that compiled C# into JavaScript. While innovative, Script# required developers to abandon JavaScript entirely and introduced significant tooling overhead, limiting its adoption. The lessons from that project, however, provided valuable insights.
Armed with that experience, Microsoft took a new approach. Led by Anders Hejlsberg, the lead architect behind C#, they introduced TypeScript in 2012. Instead of replacing JavaScript, they built on top of it. As a superset, all valid JavaScript code is also valid TypeScript, enabling gradual adoption.
That compatibility proved to be TypeScriptâs killer feature. Existing codebases could migrate file by file rather than being rewritten from scratch. JavaScript developers could keep writing familiar syntax and gradually introduce TypeScriptâs features.
TypeScript wasnât designed to make JavaScript statically typed. Its primary goal was to improve the developer experience. By catching bugs earlier, providing IntelliSense, enhancing code navigation, and supporting better tooling, it addressed JavaScriptâs pain points without sacrificing flexibility.
A pivotal moment in TypeScriptâs trajectory came in 2014 when the Angular team decided to rewrite the popular AngularJS framework using TypeScript, resulting in Angular version 2. This move was crucial in accelerating TypeScriptâs widespread adoption.
Today, TypeScript is deeply integrated into modern software development. Itâs supported by major frameworks like Angular, Next.js, and Vue, and used across both front-end and back-end codebases. With approximately 84 million weekly downloads on npm, TypeScript has evolved from a helpful tool into an industry standard.
How The TypeScript Compiler Works
At the heart of TypeScript is the TypeScript Compiler (tsc
). While it's often called a compiler, it's technically a transpiler, since it doesnât turn code into machine instructions like C or Rust would. Instead, it converts TypeScript into plain JavaScript that browsers and Node.js can understand.
Hereâs what happens under the hood when you run tsc
:
1. Type Checking
First, TypeScript checks your code for type errors. It compares your code against the types youâve defined (or that it has inferred) and surfaces any mismatches, like trying to call a method on a number that doesnât exist.
2. Generating The Abstract Syntax Tree (AST)
Before making any changes to the code, TypeScript generates an Abstract Syntax Tree (AST). This is an in-memory representation of the code's structure, where the code is parsed and broken down into nodes that represent various language constructs, such as variables, functions, or expressions.
To help visualize what an AST looks like, I used a VSCode extension that lets you explore the AST of a given file. Here's an example using a random file I found:

The AST serves two key purposes:
- Type Checking: TypeScript uses the AST for type inference and to analyze the types in your code, comparing them to the expected types and catching errors, like calling methods on the wrong data type.
- Code Transformation: TypeScript also uses the AST to transform the code. These transformations include the next two steps: type stripping and transpilation.
3. Stripping Type Annotations
Since JavaScript doesnât support types, TypeScript strips out anything it added, like type annotations or interfaces. For example, let x: number
becomes just let x
.
4. Transpiling Modern JavaScript Features
Depending on your target
option in tsconfig.json
, TypeScript may convert modern JavaScript syntax (such as async/await
, arrow functions, or const
/let
) into older JavaScript that works in legacy environments. This is similar to how Babel helps JavaScript use cutting-edge features while ensuring compatibility with older browsers.
5. Outputting JavaScript
Finally, it writes the result to a .js
or .jsx
file. That output can be run in any JavaScript environment, whether it's the browser, Node.js, or a serverless function.
Getting started
To start working with TypeScript, we need to install the TypeScript package, either locally or globally, using your package manager of choice. Here, we'll install it globally with npm:
The typescript
package includes the TypeScript compiler (tsc
), the language server (tsserver
) that powers editor features like IntelliSense and autocompletion, as well as core libraries, type definitions, and compiler utilities.
Letâs create a simple script.ts
file:
Now, while in the same directory as script.ts
run:
You'll notice that a script.js
file was generated. This is the output of the TypeScript compiler. If we open the file, youâll notice the contents are identical. Thatâs because the original code contains no TypeScript-specific syntax, and since TypeScript is a superset of JavaScript, valid JavaScript compiles without changes.
Letâs now add some TypeScript-specific syntax, recompile, and check the output. Donât worry if it looks unfamiliar, weâll break it down later:
Run it again:
and open up script.js
Notice that the type annotations are gone (tsc
strips them out entirely during compilation).
Now, let's modify our script.ts
to include a type mismatch:
If we now run tsc script.ts
, we'll see the following error in the terminal:
However, even with that error, TypeScript still emits a script.js
file:
And running it with node script.js
weâll see it outputs 5
.
Why did it compile even with an error? This is because of the nature of tsc
and how it separates the type checking and compilation step.
By default, TypeScript performs type checking, then proceeds with compilation, even if it finds errors. This means it will still emit JavaScript unless you explicitly tell it not to (with the noEmitOnError
flag). While you could pass flags individually through the command line, that quickly becomes cumbersome. Thatâs where the tsconfig.json
file comes in.
tsconfig.json
Configuring Your TypeScript Project With The tsconfig.json
file tells the TypeScript compiler how to process your project. It defines the root files, sets compiler options, and controls how TypeScript behaves during compilation. This configuration file is conceptually similar to JavaScript's jsconfig.json
.
While we won't delve into every available option here (given the extensive nature of the topic), it's worth noting that there are some essential compiler flags that can significantly improve the developer experience and help catch bugs early. Here are some of these options inside a rudimentary tsconfig.json
:
- compilerOptions: This section contains options that control how TypeScript compiles your code.
- target: Specifies the version of JavaScript your code will be compatible with (e.g.,
es6
,es5
). TypeScript will transpile your code to match this version. - outDir: Defines the output directory where the JavaScript files generated by transpiling your
.ts
files will be saved. IfnoEmit
is set to true, TypeScript will not generate any output files, so theoutDir
directory will remain empty. - noEmit: When set to
true
, TypeScript will not emit JavaScript files. This option is common with modern tools like Vite or frameworks like Next.js, which use bundlers like esbuild or SWC to handle transpilation and bundling. These tools are faster and more optimized for modern development workflows, and they handle the transpiling more efficiently than TypeScript's own compiler. - strict: Enables all strict type-checking options, such as
strictNullChecks
,noImplicitAny
, and others. Enabling strict mode is considered a best practice and is highly recommended because it improves code quality and makes type checking more comprehensive and predictable. - baseUrl: Specifies the base directory to resolve non-relative module names. Typically, this would be your
src
folder. This allows TypeScript to find files more easily. - paths: Defines custom paths or aliases for easier imports.
- target: Specifies the version of JavaScript your code will be compatible with (e.g.,
- include: Specifies the files or directories that should be included in the compilation. By default, TypeScript will look for
.ts
files in thesrc
folder. - exclude: Specifies the files or directories that should be excluded from the compilation. This is typically used to exclude
node_modules
, which often contains external dependencies and does not need to be compiled by TypeScript.
If you're working with vanilla JavaScript and want to easily set up TypeScript for your project, you can generate a basic tsconfig.json
file by running tsc --init
. This will create a default configuration that you can customize to suit your needs.
By configuring your tsconfig.json
file properly, you can customize the behavior of TypeScript to suit your projectâs needs and integrate seamlessly with modern build systems. Enabling strict mode, defining path aliases, and configuring the other relevant flags will help ensure a more efficient, organized, and smoother development experience.
Declaration Files
With your tsconfig.json
configured, it's time to look at how TypeScript understands the types of external JavaScript code (like libraries or framework-specific features). This is where declaration files come into play.
Declaration files, identifiable by their .d.ts
extension, act as type annotations for existing JavaScript code. They contain only type information, no executable logic. For instance, writing const x = 5;
in a declaration file will throw an error, as these files are meant solely to describe types, not implement functionality. Think of them as type definitions that outline the structure of JavaScript code for TypeScript. A key feature of declaration files is that they can define global types that are accessible throughout your entire application.
When you import a JavaScript library, TypeScript canât infer types from .js
files. Declaration files fill this gap, enabling features like IntelliSense, catching mismatched argument types or missing properties.
You may be asking yourself, where do these files actually show up in a codebase? The answer is, they can appear in a few different places.
Frameworks and Global Types: Frameworks usually generate their own declaration file when scaffolding a new project. In the case of Next.js, we see a file at the root of our project called
next-env.d.ts
, which looks like this:/// <reference types="next" />/// <reference types="next/image-types/global" />// NOTE: This file should not be edited// See https://nextjs.org/docs/app/api-reference/config/typescript for more information.The
///
are called triple-slash directives, and they tell TypeScript to load types from a specific directory before your code runs. For example, Next.js extends the Fetch API with extra features like therevalidate
method ortags
property, among others. The directive ensures TypeScript pulls these enhanced types fromnode_modules/next
, making them globally recognized across your project. This way, you will see them as suggestions in your code editor.Third-Party Libraries:
Libraries Written In TypeScript: Although these libraries are authored in
.ts
, theyâre typically compiled and shipped as JavaScript. To ensure consumers still get type information, library authors emit.d.ts
files alongside the compiled.js
output. This is done by setting"declaration": true
intsconfig.json
, sotsc
generates both.js
(executable) and.d.ts
(type definition) files. The"types"
field inpackage.json
specifies the entry point for your type definitions. For example, if you set"types": "dist/index.d.ts"
, thendist
should match theoutDir
you configured in yourtsconfig.json
.JavaScript Libraries Without Type Definitions: If a JavaScript library doesnât provide its own
.d.ts
files, you can add TypeScript support using community-maintained type definitions from the@types
organization on npm. For example, install type definitions forlodash
with:npm install @types/lodash --save-dev
In reality, most developers rarely write declaration files since modern TypeScript libraries generate them automatically, and @types packages cover many popular JavaScript tools. But wait a minute, where are all these @types definitions actually coming from? Who writes and maintains them?
To answer that, letâs take a look at the DefinitelyTyped repository.
DefinitelyTyped
repository
The When Microsoft first released TypeScript, one of the biggest challenges was encouraging libraries to adopt it. Many popular libraries were written in plain JavaScript, and adding type support required time and effort that maintainers didnât always have.
To address this, Microsoft created the DefinitelyTyped repository, a massive, community-maintained collection of .d.ts
files for third-party JavaScript libraries.
Hereâs how it works:
If a JavaScript library doesnât include built-in type definitions, the community can contribute them to the
DefinitelyTyped
repo.These type definitions are published under the
@types
scope on npm. For example, the types forreact
live in@types/react
, and types forlodash
live in@types/lodash
.When you install them via:
npm install --save-dev @types/reactThe type definitions are added to your
node_modules/@types
directory, making them instantly available to your project.
You can inspect this folder in node_modules/@types
to see all the community-maintained type definitions currently installed in your project. This directory acts as a type bridge, connecting plain JavaScript libraries to the TypeScript compiler and your editorâs IntelliSense system.
While TypeScript doesnât ship with all of these definitions out of the box, itâs designed to automatically include them from @types
once installed. This makes your tooling behave as if the library was written in TypeScript, even if it wasnât.
Alright, enough internals. Letâs get our hands dirty and start writing some actual TypeScript!
Basic Concepts
The idea for this portion of the blog is to go over the most basic concepts. In Part II, we will go deeper into more advanced topics, but for now, the goal is to understand basic syntax and get comfortable using TypeScriptâs simpler features.
Implicit Inference
When you write let x: number
, you're explicitly declaring x
to be of type number
. However, TypeScript can often infer the type of a variable even without an explicit annotation, especially:
- during variable initialization
- when assigning default values to parameters
- when determining function return types
This type inference works because the TypeScript compiler builds an abstract syntax tree (AST) to analyze your code's structure and context. Inference works great in straightforward scenarios. But when TypeScript canât confidently determine the type, say, when a variable is declared without an initializer or is assigned from an ambiguous source, it defaults to the any
type. This happens unless noImplicitAny
is enabled in your tsconfig.json
, which would produce an error instead.

any
by default.But what exactly is this any
type anyways?
any
Type
The The any
type means anything goes. When you use it, you're essentially opting out of TypeScript's type system, which is ironic, since type safety is TypeScriptâs main value proposition.
Technically, this is valid:
Using the any
type is like adding a @ts-ignore
comment around every usage of the variable. The compiler wonât complain, no matter what you assign or how you use it.
Generally, you should steer clear of using any, except when you're actively converting a JavaScript project to TypeScript. If you see any in your code, it often indicates an opportunity to define a more precise type or use a safer option like unknown (we'll discuss this in Part II).
Inline Definitions
We are now going to move quickly through some basic examples of inline type definitions for some common primitives.
Variables:
TypeScript allows you to explicitly annotate the types of your variables. In these cases, the types are straightforward: boolean
, number
, and string
.
Arrays:
Both of these declare an array of numbers. Choose whichever style you prefer, bracket syntax is more common, but the generic form is equally valid.
Functions:
This function takes a string
and returns a string
.
The return type here is void
, which means the function doesnât return anything. It's commonly used for functions that perform side effects, like DOM manipulation.
You can also type destructured parameters directly, this function expects an object with a string
and a number
.
This function expects a tuple of exactly four numbers, for example, it could represent padding in clockwise order: [top, right, bottom, left]
.
With async functions, make sure to annotate the return type as Promise<T>
to clarify what the function resolves to. While TypeScript can often infer this, adding the return type makes your intent clearer.
Inline type definitions are totally fine for small or simple structures. In fact, theyâre a great way to get started quickly. But once your types get more complex (especially when reused), itâs better to extract them into a type
or interface
for clarity and maintainability. Weâll dive into those next.
interface
and type
Defining Types With TypeScript provides two main ways to define custom types: interface
and type
. They're very similar, but there's a key difference: interfaces can only describe the shape of objects, while types can represent a wider variety of structures, like unions or intersections. In order to show you how they work, letâs imagine a simple logBook
function that receives a book as a parameter.
Here, Book
is an interface describing the shape of a book object. We could alternatively use a type alias like this:
Both interface and type can describe object shapes like this. However, type aliases can also describe other kinds of types that interfaces cannot, for example, unions:
Notice the following conventions:
- Both interface and type aliases use PascalCase and names are singular.
- In TypeScript, each property is separated with semicolons (
;
) instead of commas (,
), which is the case in regular JavaScript objects. While commas are technically allowed, semicolons are the preferred convention.
An advantage of using type
and interface
over inline declarations is that the error messages are often more concise, as TypeScript will only show the name of the type or interface (like Book
) instead of the full inline declaration.
Another advantage of using named types like type
or interface
is that they can be extracted into separate files and imported as necessary. This promotes reusability, since types may come up again and again within a codebase.
Both interface and type let you define and reuse custom shapes, and you can build on top of them similarly. Since type can represent a wider range of values (including primitives, unions, and intersections), weâll use type throughout the rest of the blog.
Optional And Default Parameters
Itâs common to have optional function parameters or default values for them. TypeScript supports this with the ?
modifier and standard default syntax:
In this case, the name
is an optional argument in the greet
function, and age
has a default value of 25.
Union Types
Letâs say weâre building a small program where a user can choose a theme. You might start with something like this:
The problem? Using string
is too broad, it allows any value as long as it is a string. To fix this, we can use union types:
Now, theme
is limited to just these three values. If we try assigning "refrigerator"
, TypeScript will show an error.
Unions arenât just for literal strings, theyâre also handy when a variable can be one of several types:
This means id
can be either a string
or a number
. Super useful for APIs that might return different formats for the same field, for instance, an API that returns an ID as either "42"
or 42
.
Conclusion
TypeScript transforms JavaScriptâs flexibility and dynamic nature into a more robust, predictable experience, catching errors before they reach production, enhancing your editorâs tooling, and giving you the confidence to refactor and scale large codebases. In Part 1, we covered TypeScriptâs origins, how the compiler works, basic configuration, and core language features.
In the next installment, weâll dive deeper into more advanced topics like utility types, operators and modifiers, TypeScript with React, and generics.
Thank you for following along, see you in Part 2!
Bonus
- Highly recommend this documentary if you are interested in learning more about the story of TypeScript and how it came to be.
- Did you know? Visual Studio Code (2015) was built using TypeScript and originally focused exclusively on JavaScript/TypeScript. By integrating TypeScriptâs
tsserver
engine, it delivered groundbreaking features like IntelliSense and real-time error checking for JavaScript and TypeScript. Before 2016, editors like Eclipse or Sublime Text required fragmented, language-specific plugins for basic tooling. Microsoftâs breakthrough came when they generalizedtsserver
âs success into the Language Server Protocol (LSP). Released in 2016, LSP allowed any language to plug in its own âlanguage serverâ, decoupling editor features from hardcoded syntax rules. This turned VS Code, and eventually editors like Neovim and JetBrains, into universal tools. TypeScript didnât just improve JavaScript; it redefined how all languages interact with code editors.