Understanding Liskov Substitution Principle (LSP) in TypeScript and React
Tags: react, reactnative, javascript, programming
Published: 2023-07-27 21:03:47

Welcome to another article in the series where we demystify the SOLID principles. Today we'll focus on the Liskov Substitution Principle (LSP), an essential part of writing maintainable, robust code in object-oriented programming.

**What is Liskov Substitution Principle?**

The Liskov Substitution Principle, named after Barbara Liskov, states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program.

In simpler terms, it means that a class should be able to reference a base or derived class without affecting its behavior.

**Why is LSP important?**

When properly implemented, the Liskov Substitution Principle promotes code reusability and enhances the robustness of the software system. LSP reduces the likelihood of bugs and makes your code easier to understand and maintain.

**Liskov Substitution Principle in TypeScript**

Let's start with a simple example in TypeScript to illustrate the Liskov Substitution Principle: 

```typescript
class Bird {
    fly() {
        console.log('The bird is flying');
    }
}

class Duck extends Bird {}

const makeBirdFly = (bird: Bird) => {
    bird.fly();
}

makeBirdFly(new Bird()); // Output: The bird is flying
makeBirdFly(new Duck()); // Output: The bird is flying

```

In this example, we have a `Bird` class and a `Duck` class that extends `Bird`. The `makeBirdFly` function takes a `Bird` instance as a parameter and invokes the fly method. As per the LSP, you can replace the `Bird` instance with a `Duck` instance without affecting the correctness of the `makeBirdFly` function.

**Applying LSP in React**

Now, let's consider a scenario where the Liskov Substitution Principle is violated: We can create a `Clickable` type that includes an `onClick` prop:

```typescript
type ClickableProps = {
  onClick: () => void;
};

const Button: React.FC<ClickableProps> = ({ onClick }) => {
  return <button onClick={onClick}>Button</button>;
};

// Label doesn't have an onClick prop, because it's not meant to be clickable
const Label: React.FC = () => {
  return <label>Label</label>;
};

```

Now, we might have some logic that requires handling a click event on a component:

```typescript
type ClickHandler = (components: React.FC<ClickableProps>[]) => void;

const handleClick: ClickHandler = (components) => {
  components.forEach((Component) => {
    // We'll render the component into a "virtual DOM" to trigger the onClick
    const { onClick } = render(<Component />);
    onClick();
  });
};

```

This would work fine with `Button`, but if we try to use `Label` in this way, TypeScript won't compile because `Label` doesn't have an `onClick` prop. This demonstrates the Liskov Substitution Principle: Label cannot be used in place of `Button` because it doesn't fulfil the contract of having an `onClick` handler.

We've prevented the issue at compile-time, thanks to TypeScript's type system. This prevents us from running into a runtime error where our function tries to call `onClick` on a `Label`.

Note: We're using a `render` function to "virtually" trigger the `onClick` in this contrived example. In a real-world scenario, you would more likely pass these components around, include them in JSX, and let the user interact with them to trigger any click handlers.

Let's consider another example of a base `Button` component:

```typescript
class Button extends React.Component {
  onClick() {
    console.log('Button clicked');
  }

  render() {
    return <button onClick={this.onClick}>Button</button>
  }
}

```

Now, if we were to create a `SubmitButton` that extends `Button`:

```typescript
class SubmitButton extends Button {
  onClick() {
    console.log('Submitting...');
    super.onClick();
  }
}

```

According to LSP, any instance where `Button` is used, `SubmitButton` should also be able to be used without issues:

```typescript
const App = () => {
  return (
    <div>
      <Button />
      <SubmitButton />
    </div>
  );
}

```

**Applying LSP in React with Functional Components**

Let's consider the base `Button` component:

```typescript
import React from 'react';

const Button = ({ onClick, children }) => {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

```

This `Button` component is a simple functional component that receives an `onClick` callback and `children` as props, and renders a button.

Now, we'll create a `SubmitButton` that extends `Button` in behavior:

```typescript
import React from 'react';
import Button from './Button';

const SubmitButton = ({ onClick, children }) => {
  const handleClick = () => {
    console.log('Submitting...');
    onClick && onClick();
  };

  return (
    <Button onClick={handleClick}>
      {children}
    </Button>
  );
};

export default SubmitButton;

```
In this `SubmitButton`, we have an additional `console.log` in the `handleClick` function which will run before any other `onClick` behavior passed to the `SubmitButton`.

According to LSP, any place where `Button` is used, `SubmitButton` should also be able to be used without causing any issues:

```typescript
import React from 'react';
import Button from './Button';
import SubmitButton from './SubmitButton';

const App = () => {
  const handleClick = () => {
    console.log('Button clicked');
  };

  return (
    <div>
      <Button onClick={handleClick}>Button</Button>
      <SubmitButton onClick={handleClick}>SubmitButton</SubmitButton>
    </div>
  );
};

export default App;

```

Here, both `Button` and `SubmitButton` are used interchangeably, and `SubmitButton` performs some additional behavior before running the passed `onClick` behavior, following the Liskov Substitution Principle.

**Flaws**

The Liskov Substitution Principle (LSP) is a useful guideline in object-oriented programming that helps ensure that a subclass can replace its superclass without causing incorrect behavior or errors. However, like all principles, it's not without its limitations or criticisms. Here are some of the potential issues:

**1. Overly Restrictive:** LSP can be seen as too restrictive, especially in dynamic languages or those with more flexible type systems. Sometimes, certain subclasses inherently have different behaviors or properties that make them incompatible with the superclass, even if they share a majority of their characteristics. Strict adherence to LSP can limit the design of such classes.

**2. Misinterpretation and Over-Engineering:** LSP is often misunderstood, and a strict interpretation can lead to over-engineering. Developers might be tempted to write more complex code just to fulfill the LSP when simpler, more straightforward code could solve the problem more efficiently.

**3. Doesn't Fully Solve Inheritance Problems:** Even with LSP, problems with inheritance can still occur. For instance, a subclass might need only part of the superclass's functionality, or it might require a different implementation that doesn't fit neatly into the superclass's method signatures.

Consider an application that has a `Button` component with a click handler. Now, you want to add a `DisabledButton` that looks the same as `Button`, but doesn't respond to clicks.

```typescript
interface ButtonProps {
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ onClick }) => {
  return <button onClick={onClick}>Click me</button>;
};

const DisabledButton: React.FC<ButtonProps> = ({ onClick }) => {
  return <button disabled onClick={onClick}>Can't click me</button>;
};

// Using the components
<Button onClick={() => console.log('Button clicked!')} />
<DisabledButton onClick={() => console.log('Disabled button clicked!')} />

```

This is where it gets tricky with the Liskov Substitution Principle. According to LSP, a subclass should be substitutable for its superclass without causing any issues. In this case, even though `DisabledButton` and `Button` share the same interface (`ButtonProps`), they behave differently. If you were to replace `Button` with `DisabledButton` in your code, the `onClick` prop wouldn't function as expected, which violates the LSP.

**Wrapping Up**

The Liskov Substitution Principle is a powerful principle that helps us to write more robust and maintainable code. By adhering to this principle, we ensure that our codebase stays flexible and less prone to bugs.

Remember, SOLID principles are guidelines, not hard rules. There will be times when following these principles might not be practical. However, understanding these principles can guide you towards writing better code.