1. What is TypeScript?
TypeScript is a programming language built on top of JavaScript. It adds a type system and development tools that help describe the values your program works with. TypeScript code is checked by the TypeScript compiler, then transformed into JavaScript that browsers and JavaScript runtimes can execute.
TypeScript is a superset of JavaScript: valid JavaScript is generally valid TypeScript, and you can adopt it gradually. In practice, modern TypeScript projects use compiler settings and lint rules that encourage more explicit type safety.
// JavaScript
function greet(name) {
return `Hello, ${name}`;
}
// TypeScript
function greet(name: string): string {
return `Hello, ${name}`;
}
In the TypeScript version,
name: string describes the parameter, and
: string describes the return value. The
compiler can report a mistake such as calling
greet(42) before the program runs.
2. Why was TypeScript created?
JavaScript was designed to make interactive web pages possible and approachable. Over time, it grew far beyond the small scripts it was first known for: teams began building large applications with many modules, contributors, and long-lived codebases.
In a small script, it may be easy to remember what a function expects or what fields an object contains. In a large application, that knowledge is spread across files and teammates. A function can be changed without every caller being obvious. A response object can have an unexpected shape. A refactor can quietly leave a broken property access behind.
TypeScript was created to address the needs of large-scale JavaScript development: add static analysis and helpful editor tooling while keeping JavaScript's runtime model, ecosystem, and broad compatibility. It was publicly introduced by Microsoft in 2012 and is maintained as an open-source project.
The core idea is not to replace JavaScript's runtime. It is to catch more mistakes before runtime and make code easier for developers and tools to understand.
3. What problems does TypeScript help solve?
Wrong values passed to functions
JavaScript will let a function receive a value of any type unless your code checks it:
// JavaScript
function applyDiscount(price, percentage) {
return price - price * percentage / 100;
}
applyDiscount("100", "10"); // Coercion can hide the mistake.
TypeScript can document and check the contract:
function applyDiscount(
price: number,
percentage: number,
): number {
return price - (price * percentage) / 100;
}
applyDiscount(100, 10); // OK
applyDiscount("100", "10"); // Type error
Typos and missing properties
With plain JavaScript, a misspelled property often
produces undefined and the failure may
surface somewhere later:
// JavaScript
const user = { firstName: "Maya", email: "maya@example.com" };
console.log(user.firstname.toUpperCase()); // undefined, then a runtime error
A declared type lets the editor and compiler identify the typo at the property access:
type User = {
firstName: string;
email: string;
};
const user: User = {
firstName: "Maya",
email: "maya@example.com",
};
console.log(user.firstName.toUpperCase()); // Checked by TypeScript
console.log(user.firstname); // Type error: no such property
Unclear contracts between modules
A function signature can communicate what it accepts and returns without requiring someone to inspect every call site or implementation detail. Types are especially useful at boundaries: API clients, shared utilities, component props, and data models.
Risky refactors
When a type or function contract changes, the compiler can show many affected call sites. This does not prove the refactor is correct, but it helps uncover places that need attention before users encounter them.
Editor and navigation friction
Since types describe code structure, editors can offer more reliable autocomplete, inline documentation, rename operations, go-to-definition, and diagnostics. These features can be valuable even before a project reaches a very large size.
4. JavaScript and TypeScript side by side
Consider a function that formats a person's display name. The JavaScript version works when the object has the expected data, but the contract is implicit:
// JavaScript
function formatName(person) {
return `${person.firstName} ${person.lastName}`;
}
formatName({
firstName: "Sam",
lastName: "Rivera",
});
Add a type to make the contract visible and checked:
// TypeScript
type Person = {
firstName: string;
lastName: string;
};
function formatName(person: Person): string {
return `${person.firstName} ${person.lastName}`;
}
formatName({
firstName: "Sam",
lastName: "Rivera",
}); // OK
formatName({ firstName: "Sam" }); // Error: lastName is missing
The compiled JavaScript contains the function and
template string, but not the Person type
or annotations. They guide checking and tooling; they
are not runtime objects.
5. Types you'll use in everyday code
Object types and optional properties
type Product = {
id: string;
name: string;
price: number;
description?: string;
};
function productLabel(product: Product): string {
return `${product.name} — $${product.price.toFixed(2)}`;
}
The ? marks a property as optional. Code
must account for the possibility that
description is absent before using it.
Union types and narrowing
A union says a value can be one of several types. Use checks to narrow it to a specific case:
function formatId(id: string | number): string {
if (typeof id === "number") {
return id.toFixed(0);
}
return id.toUpperCase();
}
This is safer than choosing a broad type like
any, because TypeScript requires you to
handle each possibility before using operations that
only work for one member.
Literal unions for finite choices
type RequestState = "idle" | "loading" | "success" | "error";
function showState(state: RequestState) {
if (state === "loading") {
return "Please wait…";
}
return state;
}
showState("pending"); // Type error: not an allowed state
Generics for reusable functions
A generic lets a function preserve the relationship between its input and output types instead of losing that information:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = first([10, 20, 30]); // number | undefined
const firstName = first(["Ari", "Bo"]); // string | undefined
Discriminated unions for UI states
A tagged union is useful for representing states that have different data. It can prevent impossible combinations, such as a loading state that also claims to have an error message:
type LoadResult<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
function renderResult(result: LoadResult<string[]>): string {
switch (result.status) {
case "loading":
return "Loading…";
case "success":
return result.data.join(", ");
case "error":
return result.message;
}
}
6. Practical use cases
Typing component props
In React with TypeScript, a props type documents the component's public interface and enables editor assistance where the component is used:
type GreetingProps = {
name: string;
subtitle?: string;
};
function Greeting({ name, subtitle }: GreetingProps) {
return (
<section>
<h1>Hello, {name}</h1>
{subtitle && <p>{subtitle}</p>}
</section>
);
}
Modeling API response data
A type can describe the response your application expects and make downstream code clearer:
type ApiUser = {
id: number;
name: string;
email: string;
};
async function getUser(id: number): Promise<ApiUser> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json() as Promise<ApiUser>;
}
This annotation describes what the application expects;
it does not prove that the server
returned that shape. An API response is external data.
For robust applications, parse it as
unknown and validate it at runtime with
checks or a schema-validation library before treating
it as an ApiUser.
Safer event and DOM handling
TypeScript can help ensure that a queried element exists and is of the expected kind:
const form = document.querySelector("form");
if (!form) {
throw new Error("The form element was not found");
}
form.addEventListener("submit", (event) => {
event.preventDefault();
// form is known to be an HTMLFormElement here.
});
Sharing types across features
Types can define contracts shared by form validation,
API calls, and UI components. For example, a single
Product type can keep the cart, product
page, and checkout code aligned as the model evolves.
7. Trade-offs, limits, and common myths
TypeScript does not prevent every bug
Types can catch many mistakes, but they cannot prove that a program's business logic is correct. A program may be perfectly type-safe and still calculate the wrong price or show a confusing interface. Tests, reviews, and runtime validation remain important.
Types do not validate external data
A type annotation is not a security boundary. Values
from JSON, query strings, local storage, and user input
should be considered untrusted until checked at
runtime. Type assertions such as
value as User tell the compiler to trust
you; they do not inspect or transform the value.
There is a learning and tooling cost
Teams need to learn the type system and configure a build or type-check step. Complex types can also become harder to read than the code they are intended to clarify. Prefer types that communicate useful contracts, and avoid making a simple feature unnecessarily abstract.
TypeScript is not a different runtime
TypeScript compiles to JavaScript. It does not make code execute faster by itself, and type annotations are not generally present in the emitted JavaScript. Runtime behavior still depends on the JavaScript output and the APIs it uses.
Be cautious with any
any disables checking for that value and
can spread unchecked assumptions through your code.
Prefer unknown when a value's type is not
known yet; it requires you to narrow or validate the
value before using it.
8. How to start using TypeScript
- Learn the JavaScript fundamentals first. TypeScript builds on JavaScript rather than replacing its runtime concepts.
- Start with a small feature. Add types to function parameters, return values, component props, and data models.
-
Turn on strict checking. A
strictTypeScript configuration catches more unsafe assumptions. Existing JavaScript projects can migrate gradually. - Validate data at boundaries. Treat network responses and user-provided values as unknown until checked.
- Use inference where it is clear. You do not need to annotate every local variable; TypeScript can infer many types from their values.
For example, this variable's type is inferred as
number, so an explicit annotation is
usually unnecessary:
const retryCount = 3; // inferred as number
9. So, why TypeScript?
TypeScript helps make assumptions in JavaScript code visible and checkable. It can catch mismatched values, missing properties, and broken contracts earlier; improve editor tools; and make larger refactors easier to navigate.
It is most useful when its types clarify real relationships in your program—not when every line is covered in annotations for their own sake. Use it alongside tests and runtime validation, and keep the types as simple as the problem allows.