Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Generic Constraints
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Generic Constraints

Restricting generic types with extends and type constraints

Generic constraints let you restrict what types can be used with your generic code. Instead of accepting any type, you can require that types have specific properties or extend certain interfaces. This gives you the flexibility of generics while ensuring types meet your requirements. Let's master generic constraints!

Why Use Generic Constraints?

Sometimes generic types need to have certain properties or capabilities. Constraints ensure this.

why-constraints.ts
// Problem: Can't access properties on unconstrained generic
function logLength<T>(arg: T): void {
  // console.log(arg.length);  // ❌ Error: T might not have 'length'
}

// Solution: Add constraint with 'extends'
function logLengthConstrained<T extends { length: number }>(arg: T): void {
  console.log(arg.length);  // ✅ Works - T must have length
}

// Now works with anything that has length property
logLengthConstrained("hello");           // ✅ string has length
logLengthConstrained([1, 2, 3]);         // ✅ array has length
logLengthConstrained({ length: 5 });     // ✅ object with length property
// logLengthConstrained(42);             // ❌ Error: number doesn't have length

// Another example: Ensuring objects have specific properties
function getProperty<T, K>(obj: T, key: K): any {
  // return obj[key];  // ❌ Error: T might not have property K
}

// Solution: Constrain K to be keys of T
function getPropertyConstrained<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];  // ✅ Works - K is guaranteed to be a key of T
}

const person = { name: "Yusuf", age: 25, email: "yusuf@example.com" };

const name = getPropertyConstrained(person, "name");    // Type: string
const age = getPropertyConstrained(person, "age");      // Type: number
// const invalid = getPropertyConstrained(person, "invalid");  // ❌ Error

console.log(name);  // "Yusuf"
console.log(age);   // 25

Basic Constraints with extends

Use extends to specify that a generic type must be a subtype of another type.

Constraining to Interfaces

interface-constraints.ts
// Define an interface
interface HasId {
  id: number;
}

// Constrain generic to types that have an id
function printId<T extends HasId>(obj: T): void {
  console.log(`ID: ${obj.id}`);
}

// Works with any type that has an id
printId({ id: 1, name: "Yusuf" });           // ✅ Works
printId({ id: 2, title: "Product" });        // ✅ Works
// printId({ name: "Amina" });               // ❌ Error: missing id

// More complex constraint
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

function logTimestamps<T extends Timestamped>(obj: T): void {
  console.log(`Created: ${obj.createdAt.toISOString()}`);
  console.log(`Updated: ${obj.updatedAt.toISOString()}`);
}

const record = {
  id: 1,
  name: "User Record",
  createdAt: new Date("2024-01-01"),
  updatedAt: new Date("2024-12-25")
};

logTimestamps(record);  // ✅ Works

// Combining multiple constraints with intersection
interface Named {
  name: string;
}

function displayEntity<T extends HasId & Named>(entity: T): void {
  console.log(`${entity.name} (ID: ${entity.id})`);
}

displayEntity({ id: 1, name: "Yusuf" });  // ✅ Works
// displayEntity({ id: 1 });              // ❌ Error: missing name
// displayEntity({ name: "Amina" });      // ❌ Error: missing id

Constraining to Primitive Types

primitive-constraints.ts
// Constrain to string
function toUpperCase<T extends string>(str: T): Uppercase<T> {
  return str.toUpperCase() as Uppercase<T>;
}

const upper = toUpperCase("hello");  // Type: "HELLO"
console.log(upper);  // "HELLO"

// Constrain to number
function double<T extends number>(num: T): number {
  return num * 2;
}

console.log(double(5));    // 10
console.log(double(2.5));  // 5
// double("5");            // ❌ Error: string not assignable to number

// Constrain to array
function getFirst<T extends any[]>(arr: T): T[0] {
  return arr[0];
}

const first = getFirst([1, 2, 3]);       // Type: number
const firstStr = getFirst(["a", "b"]);   // Type: string
// getFirst("string");                   // ❌ Error: string not assignable to array

// Constrain to object
function clone<T extends object>(obj: T): T {
  return { ...obj };
}

const cloned = clone({ name: "Yusuf", age: 25 });
console.log(cloned);  // { name: "Yusuf", age: 25 }

// clone(42);        // ❌ Error: number not assignable to object
// clone("string");  // ❌ Error: string not assignable to object

Using keyof in Constraints

The keyof operator creates a union of all property keys. Combined with constraints, it ensures type-safe property access.

keyof-constraints.ts
// Safe property access with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = {
  id: 1,
  name: "Yusuf Ibrahim",
  email: "yusuf@example.com",
  age: 25
};

const name = getProperty(user, "name");    // Type: string
const age = getProperty(user, "age");      // Type: number
const id = getProperty(user, "id");        // Type: number
// const invalid = getProperty(user, "invalid");  // ❌ Error: not a key

console.log(name);  // "Yusuf Ibrahim"
console.log(age);   // 25

// Set property safely
function setProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

setProperty(user, "name", "Yusuf A. Ibrahim");  // ✅ Works
setProperty(user, "age", 26);                    // ✅ Works
// setProperty(user, "age", "twenty-six");       // ❌ Error: wrong type
// setProperty(user, "invalid", "value");        // ❌ Error: not a key

console.log(user.name);  // "Yusuf A. Ibrahim"
console.log(user.age);   // 26

// Multiple properties
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    result[key] = obj[key];
  });
  return result;
}

const subset = pick(user, "name", "email");
// Type: { name: string; email: string }

console.log(subset);  // { name: "Yusuf A. Ibrahim", email: "yusuf@example.com" }

// Omit properties
function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> {
  const result = { ...obj };
  keys.forEach(key => {
    delete result[key];
  });
  return result;
}

const withoutAge = omit(user, "age");
// Type: { id: number; name: string; email: string }

console.log(withoutAge);  // { id: 1, name: "Yusuf A. Ibrahim", email: "yusuf@example.com" }

Multiple Type Parameters with Constraints

multiple-constraints.ts
// Two constrained type parameters
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const person = { name: "Yusuf", age: 25 };
const contact = { email: "yusuf@example.com", phone: "+234-803-123-4567" };

const combined = merge(person, contact);
// Type: { name: string; age: number } & { email: string; phone: string }

console.log(combined.name);   // "Yusuf"
console.log(combined.email);  // "yusuf@example.com"

// merge({ name: "Test" }, "string");  // ❌ Error: string not assignable to object
// merge(42, { x: 10 });               // ❌ Error: number not assignable to object

// Constrain second parameter based on first
function mapObject<T extends object, K extends keyof T, U>(
  obj: T,
  key: K,
  mapper: (value: T[K]) => U
): Record<K, U> {
  return { [key]: mapper(obj[key]) } as Record<K, U>;
}

const user = { name: "Yusuf", age: 25 };

const mapped = mapObject(user, "age", (age) => age * 2);
// Type: Record<"age", number>

console.log(mapped);  // { age: 50 }

// Array constraints with relationships
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
  const length = Math.min(arr1.length, arr2.length);
  const result: [T, U][] = [];
  
  for (let i = 0; i < length; i++) {
    result.push([arr1[i], arr2[i]]);
  }
  
  return result;
}

const numbers = [1, 2, 3];
const letters = ["a", "b", "c"];

const zipped = zip(numbers, letters);
// Type: [number, string][]

console.log(zipped);  // [[1, "a"], [2, "b"], [3, "c"]]

// Group by key
function groupBy<T extends object, K extends keyof T>(
  items: T[],
  key: K
): Record<string, T[]> {
  const groups: Record<string, T[]> = {};
  
  items.forEach(item => {
    const groupKey = String(item[key]);
    if (!groups[groupKey]) {
      groups[groupKey] = [];
    }
    groups[groupKey].push(item);
  });
  
  return groups;
}

const users = [
  { name: "Yusuf", role: "admin", age: 25 },
  { name: "Amina", role: "editor", age: 30 },
  { name: "Chidi", role: "admin", age: 28 }
];

const byRole = groupBy(users, "role");
console.log(byRole);
// {
//   admin: [{ name: "Yusuf", ... }, { name: "Chidi", ... }],
//   editor: [{ name: "Amina", ... }]
// }

Generic Constraints in Classes

class-constraints.ts
// Generic class with constraint
interface Entity {
  id: number;
}

class Repository<T extends Entity> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
  }
  
  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }
  
  getAll(): T[] {
    return [...this.items];
  }
  
  remove(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return false;
    
    this.items.splice(index, 1);
    return true;
  }
}

// User type extends Entity
interface User extends Entity {
  name: string;
  email: string;
}

// Product type extends Entity
interface Product extends Entity {
  name: string;
  price: number;
}

// Create repositories
const userRepo = new Repository<User>();
const productRepo = new Repository<Product>();

// Use repositories
userRepo.add({ id: 1, name: "Yusuf", email: "yusuf@example.com" });
userRepo.add({ id: 2, name: "Amina", email: "amina@example.com" });

const user = userRepo.findById(1);
console.log(user);  // { id: 1, name: "Yusuf", email: "yusuf@example.com" }

productRepo.add({ id: 101, name: "Laptop", price: 450000 });
productRepo.add({ id: 102, name: "Mouse", price: 5000 });

const products = productRepo.getAll();
console.log(products);

// Error if type doesn't extend Entity
// class BadRepo extends Repository<string> {}  // ❌ Error: string doesn't extend Entity

// Generic class with multiple constraints
interface Comparable {
  compareTo(other: this): number;
}

class SortedList<T extends Comparable> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
    this.sort();
  }
  
  private sort(): void {
    this.items.sort((a, b) => a.compareTo(b));
  }
  
  getAll(): T[] {
    return [...this.items];
  }
}

class Price implements Comparable {
  constructor(public amount: number, public currency: string) {}
  
  compareTo(other: Price): number {
    return this.amount - other.amount;
  }
}

const prices = new SortedList<Price>();
prices.add(new Price(50000, "NGN"));
prices.add(new Price(25000, "NGN"));
prices.add(new Price(75000, "NGN"));

console.log(prices.getAll());
// Sorted by amount: [25000, 50000, 75000]

Default Type Parameters

You can provide default types for generic parameters, making them optional.

default-type-parameters.ts
// Generic with default type
interface ApiResponse<T = any> {
  success: boolean;
  data: T;
  message?: string;
}

// Can use without specifying type (defaults to 'any')
const response1: ApiResponse = {
  success: true,
  data: { user: "Yusuf" }
};

// Or specify exact type
const response2: ApiResponse<{ id: number; name: string }> = {
  success: true,
  data: { id: 1, name: "Yusuf" }
};

console.log(response2.data.name);  // "Yusuf" (type-safe)

// Function with default type parameter
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const strings = createArray(3, "hello");     // Inferred as string[]
const numbers = createArray<number>(3, 42);  // Explicitly number[]
const defaults = createArray(3, "test");     // Uses default: string[]

console.log(strings);  // ["hello", "hello", "hello"]
console.log(numbers);  // [42, 42, 42]

// Class with default type parameters
class Container<T = string> {
  constructor(private value: T) {}
  
  getValue(): T {
    return this.value;
  }
  
  setValue(value: T): void {
    this.value = value;
  }
}

const stringContainer = new Container("hello");       // Defaults to Container<string>
const numberContainer = new Container<number>(42);    // Explicitly Container<number>

console.log(stringContainer.getValue());  // "hello"
console.log(numberContainer.getValue());  // 42

// Multiple defaults with constraints
interface Result<T = any, E = Error> {
  success: boolean;
  data?: T;
  error?: E;
}

const result1: Result = {
  success: false,
  error: new Error("Something failed")
};

const result2: Result<string, string> = {
  success: false,
  error: "Custom error message"
};

const result3: Result<{ id: number }> = {
  success: true,
  data: { id: 1 }
};

Practical Examples

Example 1: Type-Safe Event System

event-system.ts
// Event must have a type property
interface Event {
  type: string;
}

// Generic event emitter with constraint
class EventEmitter<T extends Event> {
  private listeners = new Map<string, Array<(event: T) => void>>();
  
  on(eventType: T["type"], listener: (event: T) => void): void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType)!.push(listener);
  }
  
  emit(event: T): void {
    const listeners = this.listeners.get(event.type);
    if (listeners) {
      listeners.forEach(listener => listener(event));
    }
  }
  
  off(eventType: T["type"], listener: (event: T) => void): void {
    const listeners = this.listeners.get(eventType);
    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index !== -1) {
        listeners.splice(index, 1);
      }
    }
  }
}

// Define event types
interface UserLoginEvent extends Event {
  type: "user:login";
  userId: number;
  username: string;
  timestamp: Date;
}

interface UserLogoutEvent extends Event {
  type: "user:logout";
  userId: number;
  timestamp: Date;
}

type UserEvent = UserLoginEvent | UserLogoutEvent;

// Create typed emitter
const userEmitter = new EventEmitter<UserEvent>();

// Add listeners
userEmitter.on("user:login", (event) => {
  // TypeScript knows this is UserLoginEvent
  console.log(`User ${event.username} logged in`);
  console.log(`User ID: ${event.userId}`);
});

userEmitter.on("user:logout", (event) => {
  // TypeScript knows this is UserLogoutEvent
  console.log(`User ${event.userId} logged out`);
});

// Emit events
userEmitter.emit({
  type: "user:login",
  userId: 1,
  username: "Yusuf",
  timestamp: new Date()
});

userEmitter.emit({
  type: "user:logout",
  userId: 1,
  timestamp: new Date()
});

Example 2: Query Builder

query-builder.ts
// Base type must be an object
class QueryBuilder<T extends object> {
  private conditions: Array<(item: T) => boolean> = [];
  private sortKey?: keyof T;
  private sortDirection: "asc" | "desc" = "asc";
  private limitValue?: number;
  
  where(predicate: (item: T) => boolean): this {
    this.conditions.push(predicate);
    return this;
  }
  
  orderBy(key: keyof T, direction: "asc" | "desc" = "asc"): this {
    this.sortKey = key;
    this.sortDirection = direction;
    return this;
  }
  
  limit(count: number): this {
    this.limitValue = count;
    return this;
  }
  
  execute(data: T[]): T[] {
    let results = [...data];
    
    // Apply conditions
    this.conditions.forEach(condition => {
      results = results.filter(condition);
    });
    
    // Apply sorting
    if (this.sortKey) {
      results.sort((a, b) => {
        const aVal = a[this.sortKey!];
        const bVal = b[this.sortKey!];
        
        if (aVal < bVal) return this.sortDirection === "asc" ? -1 : 1;
        if (aVal > bVal) return this.sortDirection === "asc" ? 1 : -1;
        return 0;
      });
    }
    
    // Apply limit
    if (this.limitValue) {
      results = results.slice(0, this.limitValue);
    }
    
    return results;
  }
}

// Define data type
interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

const products: Product[] = [
  { id: 1, name: "Laptop", price: 450000, category: "Electronics", inStock: true },
  { id: 2, name: "Mouse", price: 5000, category: "Electronics", inStock: true },
  { id: 3, name: "Desk", price: 75000, category: "Furniture", inStock: false },
  { id: 4, name: "Chair", price: 35000, category: "Furniture", inStock: true },
  { id: 5, name: "Monitor", price: 120000, category: "Electronics", inStock: true }
];

// Use query builder
const query = new QueryBuilder<Product>();

const results = query
  .where(p => p.category === "Electronics")
  .where(p => p.inStock === true)
  .orderBy("price", "desc")
  .limit(2)
  .execute(products);

console.log(results);
// [
//   { id: 1, name: "Laptop", price: 450000, ... },
//   { id: 5, name: "Monitor", price: 120000, ... }
// ]

// Another query
const cheapFurniture = new QueryBuilder<Product>()
  .where(p => p.category === "Furniture")
  .where(p => p.price < 50000)
  .execute(products);

console.log(cheapFurniture);
// [{ id: 4, name: "Chair", price: 35000, ... }]

Example 3: Validation Framework

validation-framework.ts
// Validator interface
interface Validator<T> {
  validate(value: T): { valid: boolean; error?: string };
}

// Generic validation builder
class ValidationBuilder<T extends object> {
  private validators = new Map<keyof T, Validator<any>[]>();
  
  addRule<K extends keyof T>(
    field: K,
    validator: Validator<T[K]>
  ): this {
    if (!this.validators.has(field)) {
      this.validators.set(field, []);
    }
    this.validators.get(field)!.push(validator);
    return this;
  }
  
  validate(obj: T): { valid: boolean; errors: Record<string, string[]> } {
    const errors: Record<string, string[]> = {};
    let valid = true;
    
    this.validators.forEach((validators, field) => {
      const fieldErrors: string[] = [];
      const value = obj[field];
      
      validators.forEach(validator => {
        const result = validator.validate(value);
        if (!result.valid && result.error) {
          fieldErrors.push(result.error);
          valid = false;
        }
      });
      
      if (fieldErrors.length > 0) {
        errors[field as string] = fieldErrors;
      }
    });
    
    return { valid, errors };
  }
}

// Create validators
const minLength = (min: number): Validator<string> => ({
  validate(value: string) {
    if (value.length >= min) {
      return { valid: true };
    }
    return { valid: false, error: `Must be at least ${min} characters` };
  }
});

const maxLength = (max: number): Validator<string> => ({
  validate(value: string) {
    if (value.length <= max) {
      return { valid: true };
    }
    return { valid: false, error: `Must be at most ${max} characters` };
  }
});

const email: Validator<string> = {
  validate(value: string) {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (regex.test(value)) {
      return { valid: true };
    }
    return { valid: false, error: "Invalid email format" };
  }
};

const min = (minValue: number): Validator<number> => ({
  validate(value: number) {
    if (value >= minValue) {
      return { valid: true };
    }
    return { valid: false, error: `Must be at least ${minValue}` };
  }
});

// Define form type
interface RegistrationForm {
  username: string;
  email: string;
  password: string;
  age: number;
}

// Build validator
const validator = new ValidationBuilder<RegistrationForm>()
  .addRule("username", minLength(3))
  .addRule("username", maxLength(20))
  .addRule("email", email)
  .addRule("password", minLength(8))
  .addRule("age", min(18));

// Validate data
const validData: RegistrationForm = {
  username: "yusuf",
  email: "yusuf@example.com",
  password: "securepassword",
  age: 25
};

const result1 = validator.validate(validData);
console.log(result1);  // { valid: true, errors: {} }

const invalidData: RegistrationForm = {
  username: "ab",
  email: "invalid-email",
  password: "short",
  age: 15
};

const result2 = validator.validate(invalidData);
console.log(result2);
// {
//   valid: false,
//   errors: {
//     username: ["Must be at least 3 characters"],
//     email: ["Invalid email format"],
//     password: ["Must be at least 8 characters"],
//     age: ["Must be at least 18"]
//   }
// }

Test Your Knowledge

Type Safety Check

Which generic constraint is correct?

// Option A
function getLength<T>(arg: T): number {
  return arg.length;
}

// Option B
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

// Option C
function getLength<T extends string | Array>(arg: T): number {
  return arg.length;
}

// Option D
function getLength<T>(arg: T extends { length: number }): number {
  return arg.length;
}

Common TypeScript Error

Code with Error
// Function without constraint - error!
function printLength<T>(arg: T): void {
  console.log(arg.length);  // ❌ Error: Property 'length' doesn't exist on type 'T'
}

// Trying to use it
printLength("hello");     // We know strings have length
printLength([1, 2, 3]);   // We know arrays have length
printLength({ x: 10 });   // This doesn't have length - should error!

❌ Property 'length' does not exist on type 'T'

What's Wrong?

Without a constraint, TypeScript doesn't know if the generic type T has a 'length' property. Even though some types like strings and arrays have it, TypeScript needs a guarantee.

Corrected Code
// Function with constraint - works!
function printLength<T extends { length: number }>(arg: T): void {
  console.log(arg.length);  // ✅ Safe - T is guaranteed to have length
}

// Now type-safe
printLength("hello");      // ✅ Works - string has length
printLength([1, 2, 3]);    // ✅ Works - array has length
// printLength({ x: 10 }); // ❌ Error - object doesn't have length (caught at compile-time!)

// Or more specific constraint
function printArrayLength<T>(arg: T[]): void {
  console.log(arg.length);  // ✅ Safe - arrays always have length
}

printArrayLength([1, 2, 3]);        // ✅ Works
printArrayLength(["a", "b"]);       // ✅ Works
// printArrayLength("string");      // ❌ Error - string is not an array

Solution: Add a constraint to ensure T has a length property

Best Practices

  1. Use constraints to ensure type safety - specify requirements
  2. Prefer specific constraints over loose ones
  3. Use keyof for property access - type-safe keys
  4. Combine constraints with intersections for multiple requirements
  5. Provide defaults when appropriate - better DX
  6. Document complex constraints with comments
  7. Keep constraints minimal - only what's needed
  8. Test with various types to verify constraints work
best-practices-example.ts
// ✅ Good: Specific constraint
function printId<T extends { id: number }>(obj: T): void {
  console.log(`ID: ${obj.id}`);
}

// ✅ Good: keyof constraint for type-safe access
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// ✅ Good: Multiple constraints with intersection
interface HasId { id: number; }
interface HasName { name: string; }

function display<T extends HasId & HasName>(obj: T): void {
  console.log(`${obj.name} (${obj.id})`);
}

// ✅ Good: Default type parameter
interface Response<T = any> {
  data: T;
  status: number;
}

// ✅ Good: Constrain to object for spread
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

// ❌ Avoid: Too loose constraint
function badExample<T extends any>(value: T): void {
  // 'extends any' is redundant - all types extend any
}

// ❌ Avoid: Over-constraining
function tooSpecific<T extends string & number>(value: T): void {
  // Impossible: nothing can be both string AND number
}

// ✅ Better: Use union for OR
function better<T extends string | number>(value: T): void {
  // T can be string OR number
}

Key Takeaways

  • Generic constraints restrict what types can be used
  • Use extends to specify type requirements
  • keyof creates type-safe property access
  • Constraints ensure generic code can safely use properties/methods
  • Multiple constraints use intersection types (&)
  • Default type parameters make generics more flexible
  • Constraints work with functions, classes, and interfaces
  • T extends U means T must be a subtype of U
  • Use specific constraints for better type safety
  • Constraints enable powerful, reusable patterns

What's Next?

Excellent work! You've mastered generic constraints and can now create sophisticated, type-safe generic code. Next, we'll explore Utility Types—TypeScript's built-in generic types like Partial, Pick, Omit, and Record. These powerful tools let you transform and manipulate types easily. This completes the Generics category!

Get ready to master TypeScript's utility type toolkit!

Mastering TypeScript generic constraints! Check this out!

Previous
Introduction to Generics
Next
Utility Types

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo