Volume 4

Q202: Structural Typing, Assignability and Type Compatibility

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to see whether you understand how TypeScript decides if one value can be used as another type.

They are looking for structural typing, excess property checks, function compatibility, variance, branded types, and how these rules affect API design in frontend systems.

Short Interview Answer

TypeScript uses structural typing, so compatibility is based on the shape of a value rather than the declared name of its type. If a value has the required properties with compatible types, it can usually be assigned. This makes TypeScript ergonomic for JavaScript, but it also means we need care around accidental compatibility, excess properties, callback variance, and IDs that should not be mixed.

Detailed Interview Answer

In nominal type systems, type names matter.

In TypeScript, shape matters.

type User = {
  id: string;
  name: string;
};

type Customer = {
  id: string;
  name: string;
};

const customer: Customer = { id: "c1", name: "Ada" };
const user: User = customer;

This is valid because the structure is compatible.

Why Structural Typing Fits JavaScript

JavaScript code often passes object literals, partial data, callbacks, and library objects without explicit class hierarchies.

Structural typing supports that style.

It works well for:

React props
API response shapes
utility functions
plain objects
adapter objects
test doubles
dependency injection

You can provide an object that has the needed capabilities without inheriting from a specific class.

Assignability

Assignability asks whether a value of one type can be used where another type is expected.

type HasName = {
  name: string;
};

const fullUser = {
  id: "u1",
  name: "Krishna",
  role: "admin",
};

const named: HasName = fullUser;

This is valid because fullUser has at least the required name property.

Extra properties are allowed when assigning from an existing variable.

Excess Property Checks

Object literals get an extra check.

type ButtonProps = {
  label: string;
  disabled?: boolean;
};

const props: ButtonProps = {
  label: "Save",
  disabeld: true,
};

The typo is caught because the object literal is being assigned directly.

If you assign through a variable, the rules can differ.

const rawProps = {
  label: "Save",
  disabeld: true,
};

const props: ButtonProps = rawProps;

This may pass because rawProps has the required label.

Senior engineers know excess property checks are helpful but not full validation.

Compatibility Is Not Runtime Validation

Types disappear at runtime.

This means:

type Settings = {
  theme: "light" | "dark";
};

const settings = await fetch("/api/settings").then((res) => res.json()) as Settings;

The cast does not validate JSON.

For external data, use runtime validation or explicit guards.

function isSettings(value: unknown): value is Settings {
  return (
    typeof value === "object" &&
    value !== null &&
    "theme" in value &&
    (value.theme === "light" || value.theme === "dark")
  );
}

Structural typing helps after data is trusted.

It does not make untrusted data true.

Function Compatibility

Functions are compatible based on parameter and return types.

type ClickHandler = (event: MouseEvent) => void;

const logEvent = (event: Event) => {
  console.log(event.type);
};

const handler: ClickHandler = logEvent;

This is generally safe because a function that accepts a broader Event can handle a MouseEvent.

Return types are usually covariant.

type CreateUser = () => { id: string };

const createAdmin = () => ({
  id: "u1",
  role: "admin",
});

const createUser: CreateUser = createAdmin;

Returning more information is fine when the caller only requires less.

Callback Variance

Variance is where senior TypeScript discussions often become interesting.

For callbacks, unsafe assumptions can happen if a function expects a narrow input but is used where a wider input may arrive.

type Animal = { name: string };
type Dog = { name: string; bark(): void };

let handleAnimal: (animal: Animal) => void;
let handleDog: (dog: Dog) => void = (dog) => dog.bark();

Assigning handleDog where any animal may be passed would be unsafe.

Strict function types help catch this class of issue.

Branded Types

Structural typing can accidentally allow values that should stay separate.

type UserId = string;
type ProductId = string;

function loadUser(id: UserId) {}

const productId: ProductId = "p1";
loadUser(productId);

This passes because both are strings.

A branded type can create intentional separation.

type Brand<T, TBrand extends string> = T & { readonly __brand: TBrand };

type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;

function loadUser(id: UserId) {}

Now IDs are harder to mix accidentally.

Use branding for important domain identifiers, not every string.

React Props Example

Structural typing is why prop extraction works well.

type ToolbarButtonProps = {
  label: string;
  onPress: () => void;
  disabled?: boolean;
};

function SaveButton(props: ToolbarButtonProps) {
  return <button disabled={props.disabled}>{props.label}</button>;
}

Any object with the right shape can become props.

This helps composition but means prop contracts must be precise.

API Design Implications

When designing public types:

require only what the function uses
avoid broad object types that accept too much
prefer explicit unions for states
use readonly when mutation is not intended
brand important IDs when mixing is dangerous
validate external data before trusting shape

Structural typing rewards minimal, capability-based interfaces.

Common Mistakes

Common mistakes include:

assuming same type name is required
assuming casts validate data
forgetting excess property checks only cover direct literals
mixing IDs because all IDs are strings
using huge shared interfaces instead of smaller capability types
ignoring callback parameter compatibility

The biggest mistake is treating TypeScript as nominal when it is structural.

Senior Trade-Offs

Structural typing gives flexibility.

It also allows accidental compatibility.

The senior decision is where to keep flexibility and where to add boundaries.

Use plain structural types for UI props and utilities.

Use branded types, validation, and narrower APIs for business-critical domain data.

Interview Framing

In an interview, say:

TypeScript checks compatibility by shape. That works naturally with JavaScript and React, but I add stronger boundaries for external data and domain identifiers because structural compatibility can otherwise be too permissive.

That answer balances ergonomics and correctness.

Revision Notes

  • TypeScript is structurally typed.
  • Assignability is based on compatible shape.
  • Direct object literals get excess property checks.
  • Type assertions do not validate runtime data.
  • Function parameters and return types have compatibility rules.
  • Branded types can prevent accidental ID mixing.

Follow-Up Questions

  • What is the difference between structural and nominal typing?
  • Why can extra properties be assigned through variables?
  • When would you use branded types?
  • Why are type assertions not runtime validation?
  • How does structural typing help React component props?

How I Would Answer This In A Real Interview

I would explain that TypeScript cares about whether a value has the required shape, not whether it was declared with the same type name. That makes it very practical for React props, utility functions, and plain JavaScript objects. I would also mention the risk: two values can be accidentally compatible, especially IDs or external JSON. For important boundaries, I would use runtime validation, narrower interfaces, and sometimes branded types.