Skip to content

Fix: TypeScript Generic Type Constraint Errors

FixDevs ·

Quick Answer

How to fix TypeScript generic constraint errors — Type 'X' does not satisfy the constraint 'Y', generic inference failures, constrained generics with extends, and conditional types.

The Error

You write a generic TypeScript function or class and get:

Type 'string' does not satisfy the constraint 'number'.

Or:

Type 'X' does not satisfy the constraint 'Y'.
  Type 'string' is not assignable to type 'number'.

Or:

Argument of type 'string' is not assignable to parameter of type 'T extends object'.

Or TypeScript infers unknown or {} when you expected a more specific type:

Type 'unknown' is not assignable to type 'string'.

Why This Happens

TypeScript generics use the extends keyword to add constraints — the generic type parameter must be assignable to the constraint type. Errors occur when:

  • The passed type does not satisfy the constraint — e.g., passing string where T extends object is required.
  • TypeScript cannot infer the generic type and falls back to unknown or {}.
  • The constraint is too narrow — requires a specific structure that the type does not have.
  • The constraint is too wideT extends object allows null in some versions or unexpected types.
  • Conditional types resolve incorrectlyT extends X ? A : B behaves unexpectedly with union types.

Fix 1: Understand extends in Generic Constraints

extends in a generic constraint means “is assignable to” — not class inheritance:

// T must be assignable to string (only string and string literal types)
function echo<T extends string>(value: T): T {
  return value;
}

echo("hello");     // OK — string satisfies string
echo(42);          // Error — number does not satisfy string
echo(true);        // Error — boolean does not satisfy string

// T must be an object with a name property
function greet<T extends { name: string }>(obj: T): string {
  return `Hello, ${obj.name}`;
}

greet({ name: "Alice", age: 30 }); // OK — has name: string
greet({ age: 30 });                 // Error — missing name property
greet("Alice");                     // Error — string is not an object

The constraint defines the minimum requirements for T. Any type that satisfies those requirements is accepted.

Fix 2: Fix “Type Does Not Satisfy the Constraint”

The passed type is missing required properties or is the wrong kind:

Broken — passing wrong type:

function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

getProperty("hello", "length"); // Error: string does not satisfy constraint 'object'
// string is a primitive, not an object

Fixed — pass an object:

getProperty({ name: "Alice" }, "name"); // OK

// If you need to support strings, widen the constraint:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

getProperty("hello", "length"); // Now OK — T inferred as string

Common constraint patterns and their meanings:

// T must be an object (not primitive)
function process<T extends object>(value: T): T { ... }

// T must be a string or number
function compare<T extends string | number>(a: T, b: T): boolean { ... }

// T must have specific properties
function serialize<T extends { id: number; toJSON(): string }>(item: T): string {
  return item.toJSON();
}

// T must be a constructor function
function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

// T must be a key of another type
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;
}

Fix 3: Fix Generic Inference Failures

TypeScript sometimes infers unknown or {} when it cannot determine the generic type:

Broken — inference fails:

function identity<T>(value: T): T {
  return value;
}

const result = identity([]); // T inferred as never[] — empty array
result.push("string");        // Error: Argument of type 'string' is not assignable to 'never'

Fixed — provide explicit type argument:

const result = identity<string[]>([]); // T explicitly set to string[]
result.push("string"); // OK

Broken — inference across function calls:

async function fetchData<T>(): Promise<T> {
  const response = await fetch("/api/data");
  return response.json(); // Returns Promise<any> — T is not connected
}

const data = await fetchData(); // T inferred as unknown
data.name;                      // Error: 'data' is of type 'unknown'

Fixed — provide type argument:

interface User { name: string; email: string; }

const data = await fetchData<User>(); // Explicitly specify T
data.name; // OK — TypeScript knows it's a User

Pro Tip: When TypeScript infers a generic type as unknown or {}, it means the inference engine does not have enough information. This is usually a signal to either provide an explicit type argument or restructure the function to give TypeScript more context.

Fix 4: Fix Constraints with Multiple Type Parameters

When multiple type parameters interact, constraints can become complex:

Broken — K not constrained to T’s keys:

function mapValues<T, K, V>(obj: T, key: K, value: V) {
  obj[key] = value; // Error — K is not constrained to keyof T
}

Fixed — constrain K to keyof T:

function mapValues<T extends object, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]  // Value must match the type of obj[key]
): T {
  obj[key] = value;
  return obj;
}

const user = { name: "Alice", age: 30 };
mapValues(user, "name", "Bob");    // OK
mapValues(user, "age", 31);        // OK
mapValues(user, "name", 42);       // Error — number not assignable to string
mapValues(user, "email", "[email protected]"); // Error — 'email' not a key of user

Fix 5: Fix Conditional Type Behavior with Unions

Conditional types (T extends X ? A : B) distribute over union types by default, which can produce unexpected results:

Unexpected distribution:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;          // true
type B = IsString<number>;          // false
type C = IsString<string | number>; // boolean (true | false) — distributes over union!

IsString<string | number> distributes to IsString<string> | IsString<number> = true | false = boolean.

Prevent distribution by wrapping in a tuple:

type IsString<T> = [T] extends [string] ? true : false;

type C = IsString<string | number>; // false — [string | number] extends [string]? No.
type D = IsString<string>;          // true

Use distributed conditional types intentionally:

// Extract only string types from a union
type OnlyStrings<T> = T extends string ? T : never;

type Result = OnlyStrings<string | number | boolean>; // string

Fix 6: Fix extends with Interfaces and Classes

extends in generics works with interfaces and classes based on structural typing — TypeScript checks shape, not identity:

interface Serializable {
  serialize(): string;
}

class User implements Serializable {
  constructor(public name: string) {}
  serialize() { return JSON.stringify({ name: this.name }); }
}

class Config {
  constructor(public key: string, public value: string) {}
  serialize() { return `${this.key}=${this.value}`; }
  // No 'implements Serializable' — but structurally compatible
}

function save<T extends Serializable>(item: T): string {
  return item.serialize();
}

save(new User("Alice")); // OK — User implements Serializable
save(new Config("a", "b")); // OK — Config is structurally compatible
save({ serialize: () => "data" }); // OK — inline object is compatible
save("string"); // Error — string does not have serialize()

Constraint error with this type:

class Builder<T extends Builder<T>> {
  build(): T {
    return this as unknown as T; // Requires cast
  }
}

class UserBuilder extends Builder<UserBuilder> {
  setName(name: string): this { // 'this' type narrows correctly
    return this;
  }
}

Fix 7: Fix “Type Instantiation Is Excessively Deep”

Complex nested generics can hit TypeScript’s recursion limit:

Type instantiation is excessively deep and possibly infinite.

Simplify recursive types with a depth limit:

// Broken — infinitely recursive
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// Fixed — limit recursion depth
type DeepReadonly<T, Depth extends number = 5> =
  Depth extends 0
    ? T
    : { readonly [K in keyof T]: T[K] extends object
        ? DeepReadonly<T[K], [-1, 0, 1, 2, 3, 4][Depth]>
        : T[K] };

Or use a library like type-fest which provides battle-tested deep utility types:

npm install type-fest
import type { ReadonlyDeep } from "type-fest";

type Config = ReadonlyDeep<{
  database: { host: string; port: number };
  cache: { ttl: number };
}>;

Common Generic Constraint Patterns

// Ensure T is not null or undefined
function nonNull<T extends NonNullable<T>>(value: T): T { ... }

// Ensure T is a class constructor
function mixins<T extends new (...args: any[]) => {}>(Base: T) { ... }

// Ensure T has a length property
function first<T extends { length: number; [index: number]: any }>(arr: T): T[0] {
  return arr[0];
}

// Ensure T is a Promise
function unwrap<T extends Promise<unknown>>(promise: T): Awaited<T> { ... }

// Constrain to specific record shapes
function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {
  return { ...a, ...b };
}

Still Not Working?

Use satisfies operator (TypeScript 4.9+) to validate a value against a type without widening it:

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
} satisfies Record<string, string | number[]>;

// palette.red is still number[], not string | number[]
// satisfies checks the shape but preserves the literal type

Use infer in conditional types to extract types within constraints:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type Result = ReturnType<Fn>; // string

Enable strict mode for better generic error messages — "strict": true in tsconfig.json catches more type errors earlier. For related TypeScript errors, see Fix: TypeScript Type Not Assignable and Fix: TypeScript Object is Possibly Undefined.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles