Assignment Compatibility in TypeScript

TypeScript uses structural typing to decide if one type can be assigned to another. Learn the rules of assignment compatibility for objects, functions, and primitives.

5 min read

Assignment compatibility is the set of rules TypeScript uses to decide whether a value of one type can be assigned to a variable, parameter, or property of another type. Unlike languages such as Java or C# that use nominal typing where type names determine compatibility, TypeScript uses structural typing. The compiler compares shapes, not names.

If a type has at least all the properties that the target type requires, with compatible types for each property, the assignment is allowed. Extra properties do not cause errors in normal assignment. This matches how JavaScript works at runtime, where objects carry extra data that callers simply ignore.

The Basic Rule

The core rule of structural typing is simple: a source type is assignable to a target type if the source has all the members that the target requires, with matching types for each member. The source can have more members than the target, but never fewer.

typescripttypescript
interface Pet {
  name: string;
}
 
const dog = { name: "Lassie", owner: "Rudd Weatherwax" };
const pet: Pet = dog; // OK -- dog has name, extra owner is fine

The dog object has a name property of type string, which satisfies the Pet interface. The extra owner property is simply ignored. This is the defining characteristic of structural typing: compatibility is determined by structure, not by declaration.

The same rule applies to function arguments. When you pass an object to a function that expects a specific type, the compiler checks that the argument has at least the required properties.

The Excess Property Check

Object literals get stricter treatment. When you pass an object literal directly to a function or assign it directly to a typed variable, TypeScript performs an excess property check. It flags any property that is not in the target type.

typescripttypescript
interface Pet {
  name: string;
}
 
const pet: Pet = { name: "Lassie", owner: "Rudd" };
// Error: 'owner' does not exist in type 'Pet'

This check catches typos and mistaken property names early. If you first assign the object to a separate variable and then pass that variable, the excess property check is bypassed. The reasoning is that a named variable might be used for multiple purposes, and the extra properties are intentional.

Use the direct literal check to your advantage. It catches bugs where you misspell a property name or include a property from a different type by mistake. When the check gets in the way of valid code, assign to an intermediate variable or use a type assertion.

Function Parameter Compatibility

Functions follow slightly different rules. A function with fewer parameters is assignable to a type that expects a function with more parameters. This is because JavaScript callbacks often receive more arguments than they use.

typescripttypescript
const items = [1, 2, 3];
items.forEach((item) => console.log(item));
// OK -- forEach passes 3 args, but callback only uses 1

The forEach method provides the element, index, and array to the callback. But a callback that only declares the element parameter is perfectly valid. TypeScript allows this because extra arguments are simply ignored at runtime.

Return types work the other way. A source function's return type must be assignable to the target function's return type. If the target expects a return of type A, the source must return A or a subtype of A with more properties.

Primitives and Special Types

Primitive types are compatible only with themselves or with union types that include them. A string is assignable to string, to a string literal type if the value matches exactly, and to a union that includes string.

A string is not assignable to number, even though JavaScript would coerce it at runtime. TypeScript prevents these implicit conversions.

The special types any, unknown, and never have their own compatibility rules. Any is assignable to and from everything, which is why it disables type checking. Unknown is safe: anything can be assigned to unknown, but unknown can only be assigned to any.

Never is the empty type: it is assignable to everything, but nothing is assignable to it except never itself.

Readonly Compatibility

Readonly properties have special assignment behavior. A mutable object can be assigned to a readonly type, but a readonly type cannot be assigned to a mutable type. This is a one-way street designed to prevent accidental mutation.

typescripttypescript
const mutable: number[] = [1, 2, 3];
const frozen: readonly number[] = mutable; // OK
const back: number[] = frozen;
// Error: 'readonly number[]' not assignable to 'number[]'

The same rule applies to properties. A writable property satisfies a readonly property requirement, but not the reverse. This lets you safely pass mutable data to functions that promise not to modify it, while preventing functions from treating readonly data as mutable.

For more on how readonly works with properties and arrays, see Readonly values in TypeScript. For how TypeScript infers types that affect compatibility, see Type widening in TypeScript.

Rune AI

Rune AI

Key Insights

  • TypeScript uses structural typing: shapes matter, names do not.
  • A source is assignable to a target if it has all required properties with compatible types.
  • Object literals get stricter excess property checks than variables.
  • Functions with fewer parameters can be assigned where more are expected.
  • Readonly to mutable assignment is blocked; mutable to readonly is allowed.
RunePowered by Rune AI

Frequently Asked Questions

What is structural typing?

Structural typing means TypeScript checks compatibility by comparing the shapes of types, not their names. Two types are compatible if they have the same structure, even if they have different names.

Why can I assign an object with extra properties to a variable?

TypeScript allows extra properties in most assignments because having more properties does not break code that only uses a subset. Object literals passed directly to functions get stricter excess property checking.

Can I assign a readonly array to a mutable array?

No. Readonly arrays cannot be assigned to mutable array variables because that would allow mutation through the mutable reference. The reverse (mutable to readonly) is allowed.

Conclusion

Assignment compatibility in TypeScript is based on structural typing. A value is compatible with a target type if it has at least the required properties with matching types. This is different from nominal type systems where names matter. The key rules to remember: extra properties are allowed in most cases, functions with fewer parameters are assignable to functions expecting more, and readonly is a one-way street. Understanding these rules helps you predict when TypeScript will accept or reject an assignment.