TypeScript · intermediate
TypeScript type not assignable: reduce the incompatible value shape
A targeted playbook for resolving the TypeScript diagnostic "Type X is not assignable to type Y" when the failure stems from a value whose static shape (fields, optionality, generics, union membership, or index signature) does not satisfy the declared target type, rather than from a missing import or runtime mismatch.
The symptoms
- •TypeScript reports a "Type X is not assignable to type Y" diagnostic whose cause line names a property or generic parameter, not "Cannot find name".
- •Assignment fails at compile time even though the value appears structurally correct in the editor, and no runtime exception is produced.
- •Hover information on the target parameter or variable lists "missing property", "is not assignable to type '...'", or a union mismatch such as "Type 'string | number' is not assignable to type 'string'".
- •The same value is accepted in one location but rejected in another, suggesting the target type differs between call sites.
- •The diagnostic persists after running the TypeScript compiler, while the surrounding tests in the same module pass.
Likely causes
- •A literal object is widened to a less precise type (e.g., string literal widened to string, or array widened to any[]) before it reaches the target, causing a union narrowing failure.
- •Optional fields are declared on the target type but the provided value omits them, or the value supplies an extra property the target type does not permit (excess property check).
- •A generic parameter is inferred to a wider type than expected (e.g., T inferred as unknown or {}), and the value's shape does not match the inferred bound.
- •A discriminated union is being assigned from a member whose discriminator field or payload differs from what the target union expects.
- •Readonly or mutable variance conflicts exist between source and target (e.g., a mutable array assigned to a readonly tuple, or a wider generic assigned to a narrower one).
- •An index signature on the target expects a specific value type, and the value supplies a property whose value type does not conform to that signature.
First ten minutes
- 01Read the full diagnostic message; record the source type, target type, and the property or generic argument TypeScript names in the cause line.
- 02Open the file holding the target type declaration and the file holding the offending value; note the exact property order and optionality markers on both sides.
- 03Hover the target parameter or variable in the editor to capture the displayed type; this is the authoritative shape the compiler is comparing against.
- 04Run the compiler on only the affected file to confirm the diagnostic is reproducible and is not a stale build artifact; record the diagnostic code (e.g., TS2322, TS2345).
- 05Compare the value's inferred type (from hover or a temporary type alias) with the target's declared type, listing every divergent property.
Evidence to collect
- •The complete compiler diagnostic text, including the file path, line, column, error code, source type, and target type.
- •The exact declared target type, including generic arguments, optional and readonly modifiers, and any union members.
- •The inferred static type of the offending value at the assignment site (from editor hover or a temporary type alias).
- •The TypeScript configuration values that influence assignability: strict, noImplicitAny, strictNullChecks, exactOptionalPropertyTypes, and noUncheckedIndexedAccess.
- •Whether the value passes through a helper function whose parameter type is wider than the target, which can widen literals before they reach the assignment.
Where to look
- •At the assignment boundary between the value expression and the target parameter, field, or variable, where the compiler narrows its view of the value.
- •In the type alias or interface declaration of the target, focusing on optional modifiers, index signatures, and generic constraints.
- •In the inferred return type of any factory or helper that produces the value, because the helper's parameter type determines widening.
- •In the tsconfig.json compilerOptions, since strictness flags directly change which shape mismatches are reported.
- •In discriminated union sites, where the discriminator field, payload shape, and exhaustiveness check each constrain assignability.
Diagnostic steps
- 01Extract source type, target type, and the property flagged in the diagnostic; if the property is optional on the target but required in the source's structural comparison, the fix is to supply the property or mark it optional in the target only when intentional.
- 02If the source type is wider than the target (e.g., string vs. a string literal union), narrow the value at the assignment site with a const assertion, a literal type, or a type guard rather than relaxing the target.
- 03If a generic parameter is inferred too wide, constrain the call site explicitly with a type argument so the compiler infers the intended bound rather than the value's widened shape.
- 04If excess property checking is the cause (the value declares a property not present on the target), either remove the extra property, widen the target type intentionally, or assign through an intermediate variable of a compatible type.
- 05If readonly or mutable variance is the cause, align the source and target mutability annotations rather than casting through any or unknown to silence the diagnostic.
- 06If an index signature on the target requires a specific value shape, confirm each supplied property's value conforms; introduce a mapped type or pick a more specific target rather than weakening the index signature.
Common mistakes
- •Adding a non-null assertion or as any cast at the assignment site without first identifying which property diverges; this suppresses the diagnostic but hides the actual shape mismatch.
- •Loosening the target type to accept the offending value (e.g., making fields optional or widening a union) when the real defect is in the value's construction site.
- •Disabling strict or noImplicitAny globally to clear the diagnostic, which masks unrelated shape checks across the codebase.
- •Assuming the value is correct because it serializes and parses cleanly at runtime; runtime data is erased by TypeScript and is not consulted during assignability checks.
- •Ignoring the difference between optional fields with exactOptionalPropertyTypes enabled and optional fields under default strictness, since the same source value can pass under one mode and fail under the other.
Safe fixes
- •Add the missing property required by the target, or remove the extra property causing the excess property check to fire, after confirming the divergence with a temporary type alias on both sides.
- •Apply a const assertion to a literal object or array so its inferred type preserves literal types instead of widening to string, number, or any[].
- •Supply an explicit type argument at the call site of a generic helper so the inferred bound matches the target rather than the value's widened shape.
- •Narrow a union member with a type guard or discriminant check before assignment, rather than widening the target's union to accommodate an unrouted case.
- •Align readonly or mutable modifiers on source and target so variance permits the assignment, instead of casting through an unrelated intermediate type.
Prove the fix
- 01The original diagnostic no longer appears when the compiler is run against the same file, and no new diagnostics are introduced on adjacent assignment sites.
- 02Editor hover on the value at the fixed site reports a type structurally equal to the target, with matching optionality and readonly modifiers.
- 03The compiler is run with strictness flags matching the project tsconfig; the fix holds under those flags, not only after relaxing them.
- 04A targeted unit test exercising the value's construction and assignment passes, and a negative test that supplies a value with a known extra property still fails as expected.
- 05The type-level proof (a temporary type alias comparing both sides) resolves to true after the change, indicating the source type is assignable to the target without casts.
Prevention and next steps
- •Define DTO and boundary types once in a shared module and import them at every assignment site so the target shape cannot drift between consumers.
- •Favor const assertions and explicit type arguments at construction sites to keep literals narrow instead of relying on widening followed by casts.
- •Keep strict, noImplicitAny, strictNullChecks, and exactOptionalPropertyTypes enabled so structural drift is reported at the boundary rather than at a later consumer.
- •Prefer narrower generic constraints and mapped types over permissive index signatures so each property is checked individually at the assignment site.
- •Add type-level unit tests for boundary converters so a regression in the inferred shape is caught by the test runner before it surfaces as a compile diagnostic elsewhere.
Safe commands and checks
tsc --noEmit -p tsconfig.json
tsc --noEmit --strict --exactOptionalPropertyTypes path/to/file.ts
tsc --noEmit --pretty false path/to/file.ts
node -e "console.log(require('typescript').version)"
grep -n "strict" tsconfig.json