TypeScript basics JavaScript developers 2026 featured image
|

TypeScript Basics in 2026: Types, Interfaces, and Generics for JavaScript Developers

TypeScript is JavaScript with types. That single addition — telling the compiler what shape your data has — eliminates entire categories of bugs that plague JavaScript applications. In 2026, TypeScript has become the default for serious JavaScript development: React, Angular, Vue, Node.js frameworks, and most npm packages ship with TypeScript support. If you understand JavaScript variables and functions, you’re ready to learn TypeScript basics.

This guide covers everything you need to start writing TypeScript: basic types, interfaces, generics, enums, type narrowing, and configuration. By the end, you’ll understand why teams adopt TypeScript and how to introduce it into existing JavaScript projects.

What Is TypeScript and Why Learn TypeScript Basics?

TypeScript is a superset of JavaScript — every valid JavaScript file is also valid TypeScript. TypeScript adds a type system that catches errors at compile time instead of runtime. When you write TypeScript, the compiler (tsc) checks your types and then strips them away, producing plain JavaScript that runs anywhere.

// JavaScript — this bug only appears at runtime
function greet(user) {
  return 'Hello, ' + user.name.toUpperCase();
}
greet({ nama: 'Alice' });  // Runtime error: Cannot read property 'toUpperCase' of undefined

// TypeScript — this bug is caught immediately
interface User {
  name: string;
  email: string;
}

function greet(user: User): string {
  return 'Hello, ' + user.name.toUpperCase();
}
greet({ nama: 'Alice' });  // Compile error: 'nama' does not exist on type 'User'

The TypeScript basics compiler catches the typo before you ever run the code. In a large codebase with hundreds of functions and data structures, this kind of safety prevents countless bugs from reaching production.

TypeScript Basics: Setting Up Your First Project

# Install TypeScript
npm install --save-dev typescript

# Initialize tsconfig.json
npx tsc --init

# Compile TypeScript to JavaScript
npx tsc

# Watch mode (recompile on changes)
npx tsc --watch

# Using with Vite (no separate tsc step needed)
npm create vite@latest my-app -- --template react-ts

The tsconfig.json file controls the TypeScript compiler. Here’s a modern configuration for 2026:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

The strict: true flag enables all strict type-checking options. Always use it for new projects — it catches the most bugs and is the TypeScript basics standard that the community recommends.

TypeScript Basics: Primitive Types and Type Annotations

TypeScript adds type annotations to JavaScript’s existing data types. You declare types using a colon after the variable name:

// Primitive types
let name: string = 'Alice';
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;

// Arrays
let scores: number[] = [95, 87, 92];
let names: Array<string> = ['Alice', 'Bob'];

// Tuple (fixed-length array with specific types)
let pair: [string, number] = ['Alice', 30];

// Any (escape hatch — avoid when possible)
let flexible: any = 'could be anything';

// Unknown (safer than any — must narrow before use)
let input: unknown = getUserInput();
if (typeof input === 'string') {
  console.log(input.toUpperCase());  // OK — TypeScript knows it's a string
}

// Void (function returns nothing)
function log(message: string): void {
  console.log(message);
}

// Never (function never returns — throws or infinite loop)
function throwError(msg: string): never {
  throw new Error(msg);
}

TypeScript also has type inference — if you initialize a variable, TypeScript infers the type automatically:

let count = 42;          // TypeScript infers: number
let greeting = 'hello';  // TypeScript infers: string
let items = [1, 2, 3];   // TypeScript infers: number[]

// No annotation needed — TypeScript figures it out
const user = {
  name: 'Alice',
  age: 30
};  // TypeScript infers: { name: string; age: number }

TypeScript Basics: Interfaces and Type Aliases

Interfaces define the shape of objects. They’re the most important TypeScript feature for structuring your data:

// Interface — defines object shape
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;            // Optional property
  readonly createdAt: Date; // Can't be changed after creation
}

// Using the interface
function createUser(data: User): User {
  return { ...data, createdAt: new Date() };
}

// TypeScript catches missing required properties
createUser({ id: 1, name: 'Alice' }); 
// Error: Property 'email' is missing

// Extending interfaces
interface Admin extends User {
  permissions: string[];
  role: 'admin' | 'superadmin';
}

const admin: Admin = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  createdAt: new Date(),
  permissions: ['users.read', 'users.write'],
  role: 'admin'
};

Type aliases offer similar functionality with different syntax and some additional capabilities:

// Type alias
type Point = {
  x: number;
  y: number;
};

// Union types (value can be one of several types)
type Status = 'pending' | 'active' | 'inactive';
type ID = string | number;

// Intersection types (combine multiple types)
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type TimestampedUser = User & Timestamped;

// Utility types
type PartialUser = Partial<User>;         // All properties optional
type RequiredUser = Required<User>;       // All properties required
type UserName = Pick<User, 'name' | 'email'>;  // Only specific properties
type UserWithoutId = Omit<User, 'id'>;    // Exclude specific properties
type ReadonlyUser = Readonly<User>;       // All properties readonly

The general guideline: use interface for object shapes (especially when they might be extended) and type for unions, intersections, and computed types.

TypeScript Basics: Functions with Types

// Typed function parameters and return type
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Optional and default parameters
function greet(name: string, greeting: string = 'Hello'): string {
  return \`\${greeting}, \${name}!\`;
}

// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

// Function type
type MathOperation = (a: number, b: number) => number;

const divide: MathOperation = (a, b) => a / b;

// Callback typing
function fetchData(url: string, callback: (data: unknown) => void): void {
  // ...
}

// Overloads — same function name, different parameter types
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  if (typeof value === 'string') return value.trim();
  return value.toFixed(2);
}

TypeScript Basics: Generics

Generics let you write functions and classes that work with any type while maintaining type safety. They’re like type parameters — placeholders that get filled in when you use the function.

// Without generics — loses type information
function first(arr: any[]): any {
  return arr[0];
}
const item = first([1, 2, 3]);  // item is 'any' — no type safety

// With generics — preserves type information
function firstGeneric<T>(arr: T[]): T {
  return arr[0];
}
const num = firstGeneric([1, 2, 3]);        // num is 'number'
const str = firstGeneric(['a', 'b', 'c']);  // str is 'string'

// Generic interface
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;

// Generic with constraints
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength('hello');      // OK — strings have length
logLength([1, 2, 3]);   // OK — arrays have length
logLength(42);           // Error — numbers don't have length

// Multiple generic parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const result = merge({ name: 'Alice' }, { age: 30 });
// result is { name: string } & { age: number }

Generics are essential for building reusable libraries, API clients, and data structures. React’s useState<T>, useRef<T>, and component props all use generics extensively.

TypeScript Basics: Enums and Literal Types

// Enum — named constants
enum Direction {
  Up = 'UP',
  Down = 'DOWN', 
  Left = 'LEFT',
  Right = 'RIGHT'
}

function move(direction: Direction): void {
  console.log(\`Moving \${direction}\`);
}

move(Direction.Up);     // OK
move('UP');             // Error — must use the enum

// Numeric enums (auto-increment)
enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

// Const enum (inlined at compile time — smaller output)
const enum Color {
  Red = '#ff0000',
  Green = '#00ff00',
  Blue = '#0000ff'
}

// String literal types (often preferred over enums)
type Theme = 'light' | 'dark' | 'system';

function setTheme(theme: Theme): void {
  document.body.className = theme;
}

setTheme('light');   // OK
setTheme('blue');    // Error — not a valid Theme

In 2026, many TypeScript developers prefer string literal unions over enums because they’re simpler, don’t generate runtime code, and work better with type narrowing.

TypeScript Basics: Type Narrowing and Guards

Type narrowing is how TypeScript tracks the type of a variable through control flow. After an if check, TypeScript knows the type has been narrowed:

// typeof narrowing
function process(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase();   // TypeScript knows: string
  }
  return value.toFixed(2);        // TypeScript knows: number
}

// instanceof narrowing
function handleError(error: Error | string): string {
  if (error instanceof Error) {
    return error.message;         // TypeScript knows: Error
  }
  return error;                   // TypeScript knows: string
}

// Discriminated unions (powerful pattern)
type Shape = 
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return 0.5 * shape.base * shape.height;
  }
}

// Custom type guard
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'name' in obj &&
    'email' in obj
  );
}

function greetSomeone(input: unknown): string {
  if (isUser(input)) {
    return \`Hello, \${input.name}\`;  // TypeScript knows: User
  }
  return 'Hello, stranger';
}

Discriminated unions (using a kind or type property) are one of the most powerful TypeScript patterns. They model state machines, API responses, and component variants with complete type safety.

TypeScript Basics with React

// Component props
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  children?: React.ReactNode;
}

function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
  return (
    <button 
      className={\`btn btn-\${variant}\`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// Typed hooks
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const inputRef = useRef<HTMLInputElement>(null);

// Typed event handlers
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
  setQuery(e.target.value);
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>): void {
  e.preventDefault();
  // ...
}

Migrating JavaScript to TypeScript Basics

You don’t need to rewrite your entire codebase. TypeScript supports gradual migration:

Step 1: Add TypeScript and a tsconfig.json with "allowJs": true. This lets .js and .ts files coexist. Step 2: Rename files one at a time from .js to .ts (or .jsx to .tsx). Step 3: Add type annotations starting with the most-used functions and interfaces. Step 4: Enable stricter compiler options gradually. Step 5: Once all files are migrated, remove "allowJs" and enable "strict": true.

// tsconfig.json for migration
{
  "compilerOptions": {
    "allowJs": true,                // Allow .js files
    "checkJs": true,                // Type-check .js files too
    "strict": false,                // Relax strict mode during migration
    "noImplicitAny": false,         // Allow implicit 'any' during migration
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

What’s Next

TypeScript basics give you a type-safe foundation. The next step is verifying your code actually works as expected through automated testing. In the next lesson on Testing Fundamentals, we’ll cover unit tests, integration tests, test-driven development, and the testing frameworks that work with both JavaScript and TypeScript.

For the tools that compile TypeScript in your build pipeline, see our guide to JavaScript Bundlers. And to enforce TypeScript best practices automatically, check out our ESLint & Prettier guide which covers TypeScript-specific linting plugins.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *