johnieskills

Fixes TypeScript errors and strengthens your types.

Resolve TypeScript errors, eliminate any, and design complex types (generics, conditional, mapped, template literal, branded/opaque). Use for type-inference problems, infer / extends questions, utility types (Partial, Record, ReturnType, Awaited, NoInfer), satisfies, function overloads, declaration merging, and strict-mode refactors.

$ npx skills add johnie/skills --skill typescript-engineer
skills/typescript-engineer/references/generics-basics.mdGitHub

Generics Fundamentals

Overview

Generics allow you to create reusable components that work with multiple types while maintaining type safety. They're essential for building flexible, type-safe APIs.

Basic Generic Functions

// Without generics - loses type information
function identity(value: any): any {
  return value;
}

// With generics - preserves type
function identity<T>(value: T): T {
  return value;
}

const num = identity(42); // Type: number (inferred)
const str = identity("hello"); // Type: string (inferred)
const explicit = identity<boolean>(true); // Type: boolean (explicit)

Inference Dependencies

When the type of one parameter depends on another, use generics:

// The return type depends on what keys exist in the config
const createComponent = <TConfig extends Record<string, string>>(
  config: TConfig
) => {
  return (variant: keyof TConfig, ...otherClasses: string[]): string => {
    return config[variant] + " " + otherClasses.join(" ");
  };
};

// TConfig is inferred as { primary: string; secondary: string }
const getButtonClasses = createComponent({
  primary: "bg-blue-300",
  secondary: "bg-green-300",
});

// variant must be "primary" | "secondary"
getButtonClasses("primary", "px-4"); // OK
getButtonClasses("tertiary", "px-4"); // Error: "tertiary" not in keys

Generic Constraints with extends

Constrain generics to ensure they have required properties:

// Unconstrained - TFunc could be anything
type WrapFunction<TFunc> = (...args: any[]) => any;

// Constrained - TFunc must be a function
type WrapFunction<TFunc extends (...args: any) => any> = (
  ...args: Parameters<TFunc>
) => ReturnType<TFunc>;

Why Constraints Matter

// Without constraint
function getLength<T>(item: T): number {
  return item.length; // Error: Property 'length' does not exist on type 'T'
}

// With constraint
function getLength<T extends { length: number }>(item: T): number {
  return item.length; // OK - we know T has length
}

getLength("hello"); // 5
getLength([1, 2, 3]); // 3
getLength({ length: 10 }); // 10
getLength(42); // Error: number doesn't have length

Default Generic Parameters

Provide defaults for optional type parameters:

type WrapFunction<
  TFunc extends (...args: any) => any,
  TAdditional = {}, // Default to empty object
> = (
  ...args: Parameters<TFunc>
) => Promise<Awaited<ReturnType<TFunc>> & TAdditional>;

// Can use without TAdditional
type BasicWrapper = WrapFunction<typeof fetchUser>;

// Or with TAdditional
type ExtendedWrapper = WrapFunction<typeof fetchUser, { meta: string }>;

Generic Slot Inference

TypeScript infers generic types from usage:

// Generic is inferred from the argument passed
const createComponent = <TConfig>(config: TConfig) => {
  return config;
};

// TConfig is inferred as { primary: string; secondary: string }
const component = createComponent({
  primary: "bg-blue-300",
  secondary: "bg-green-300",
});

When Inference Doesn't Work

If you don't USE the generic in arguments, it defaults to unknown:

// BAD - TConfig isn't used in arguments, defaults to unknown
const createComponent = <TConfig>(config: Record<string, string>) => {
  // TConfig is unknown here
};

// GOOD - TConfig IS the argument type
const createComponent = <TConfig extends Record<string, string>>(
  config: TConfig
) => {
  // TConfig is inferred from what's passed
};

Multiple Generic Parameters

Use multiple parameters for related but distinct types:

function map<TInput, TOutput>(
  items: TInput[],
  transform: (item: TInput) => TOutput
): TOutput[] {
  return items.map(transform);
}

// Both TInput and TOutput are inferred
const numbers = map(["1", "2", "3"], (s) => parseInt(s));
// TInput: string, TOutput: number, Result: number[]

Pattern: keyof with Generics

Combine keyof with generics for type-safe property access:

function getProperty<TObj, TKey extends keyof TObj>(
  obj: TObj,
  key: TKey
): TObj[TKey] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // Type: string
const age = getProperty(user, "age"); // Type: number
const invalid = getProperty(user, "email"); // Error: "email" not in keyof

Generics in Classes

class Container<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  map<U>(transform: (value: T) => U): Container<U> {
    return new Container(transform(this.value));
  }
}

const numContainer = new Container(42);
const strContainer = numContainer.map((n) => n.toString());
// strContainer is Container<string>

Complete Example: Component Factory

// A factory that creates type-safe component class generators
export const createComponent = <TConfig extends Record<string, string>>(
  config: TConfig
) => {
  // Return a function that requires valid variant keys
  return (variant: keyof TConfig, ...otherClasses: string[]): string => {
    return config[variant] + " " + otherClasses.join(" ");
  };
};

// Usage
const getButtonClasses = createComponent({
  primary: "bg-blue-500 text-white",
  secondary: "bg-gray-200 text-gray-800",
  danger: "bg-red-500 text-white",
});

// Type-safe: variant must be "primary" | "secondary" | "danger"
const classes = getButtonClasses("primary", "px-4", "py-2");
// Result: "bg-blue-500 text-white px-4 py-2"

// Type error on invalid variant
getButtonClasses("invalid"); // Error!

When to Use Generics

  • Type preservation: When you need to preserve type information through a function
  • Inference dependencies: When one type depends on another
  • Reusable components: When building APIs that work with multiple types
  • Constraints: When you need to ensure types have certain properties
  • Factory functions: When creating functions that return typed results

Common Pitfalls

Unnecessary Generics

// BAD - generic provides no value
function greet<T extends string>(name: T): string {
  return `Hello, ${name}`;
}

// GOOD - just use string
function greet(name: string): string {
  return `Hello, ${name}`;
}

Over-constraining

// BAD - overly specific constraint
function process<T extends { id: string; name: string; email: string }>(
  obj: T
): void {}

// GOOD - only require what you actually use
function process<T extends { id: string }>(obj: T): void {}

Forgetting to Constrain

// BAD - accessing property that might not exist
function getName<T>(obj: T): string {
  return obj.name; // Error: Property 'name' does not exist
}

// GOOD - constrain to types that have name
function getName<T extends { name: string }>(obj: T): string {
  return obj.name;
}