Fix: TypeScript Generic Type Constraint Errors
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
stringwhereT extends objectis required. - TypeScript cannot infer the generic type and falls back to
unknownor{}. - The constraint is too narrow — requires a specific structure that the type does not have.
- The constraint is too wide —
T extends objectallows null in some versions or unexpected types. - Conditional types resolve incorrectly —
T extends X ? A : Bbehaves 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 objectThe 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 objectFixed — 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 stringCommon 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"); // OKBroken — 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 UserPro Tip: When TypeScript infers a generic type as
unknownor{}, 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 userFix 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>; // trueUse 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>; // stringFix 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-festimport 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 typeUse 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>; // stringEnable 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: TypeScript Decorators Not Working (experimentalDecorators)
How to fix TypeScript decorators not applying — experimentalDecorators not enabled, emitDecoratorMetadata missing, reflect-metadata not imported, and decorator ordering issues.
Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
Fix: TypeScript isolatedModules Errors (const enum, type-only imports)
How to fix TypeScript isolatedModules errors — why const enum fails with Babel and Vite, how to replace const enum, fix re-exported types, and configure isolatedModules correctly for your build tool.
Fix: Vitest Setup Not Working (setupFiles, Mocks, and Global Config Issues)
How to fix Vitest configuration not taking effect — why setupFiles don't run, globals are undefined, mocks don't work, and how to configure Vitest correctly for React, Vue, and Node.js projects.