Is TypeScript's approach to inheritance in conflict with the substitution principle of object-oriented programming?

It is commonly believed that a derived class in OOP languages should be a superset of its base class, meaning it adds more things rather than removing them.

In most cases, type-casting a derived class instance to a base class works correctly only if this principle is followed.

However, TypeScript seems to deviate from this rule. In TypeScript, the types of properties in a base class can only become narrower in the derived class, which is quite unusual.

The following code snippet is allowed in TypeScript, but it may lead to issues:

class BaseClass1 {
    value!: number | string;
}

class DerivedClass1 extends BaseClass1 {
    moreValue!: string; // adding more properties is permitted
    declare value: number; // narrowing the type
}

let baseInstance: BaseClass1 = new DerivedClass1();
baseInstance.value = "hello"; // though TypeScript allows it, it may not be correct

While the code below might seem fine, TypeScript signals that there's an issue:

class BaseClass2 {
    value!: number;
}

class DerivedClass2 extends BaseClass2 {
    declare value: number | string; // incorrect - cannot widen the type
}

Does this mean I have misunderstood OOP principles? Or if my understanding is correct and TypeScript indeed violates these rules, why did it choose this approach?

If a base class property's type becomes wider in a derived class, but the methods in the base class only handle the narrower type, calling a base class method in the derived class could result in problems. However, TypeScript finds itself in a dilemma regarding this matter.

Answer №1

Expanding on the insights shared by my community colleagues in the discussions, TypeScript introduces an intriguing concept known as structural typing. Rather than rigidly enforcing class hierarchies, it evaluates types based on their composition. In the initial scenario, this feature allows you to narrow down the type of a property in the derived class, presenting a departure from conventional object-oriented programming.

By opting to specify the type as number in the first instance, TypeScript accommodates this choice due to its emphasis on flexibility, facilitating developers in extending classes or customizing types with ease.

Conversely, when confronted with the second situation, TypeScript takes a firm stance. If the subclass attempts to broaden the type beyond the scope defined by the superclass – transitioning from number to number | string, for example – TypeScript intervenes with a clear message: "Hold on, that's not permissible!" This precaution is implemented to sustain type integrity and adhere to the established contract mandated by the base class.

Therefore, TypeScript's methodology revolves around adaptability, catering to the dynamic aspects of JavaScript while striking a harmonious balance between static typing and traditional OOP paradigms.

Update:

I apologize for disregarding your original query title completely.

The approach to inheritance in TypeScript adheres closely to the Liskov Substitution Principle (LSP), a pivotal principle in object-oriented programming. According to LSP, instances of a base class should seamlessly replace instances of its derived class without compromising program correctness. Through its support for refined type definitions in specific scenarios, TypeScript upholds the LSP without breaching the fundamental agreement set forth by the parent class.

Hence, TypeScript's structural typing fosters adaptability without contravening core principles of inheritance. It enables dynamic adjustments within the confines of substitution, offering a favorable compromise between flexibility and adherence to OOP tenets.

Answer №2

  1. It is puzzling why this would be considered incorrect. As per the substitution principle, it should be permissible to assign subclasses of the type accepted by the variable. The value is defined as number | string (either a string or number), therefore assigning a string is completely valid in TypeScript and aligns with the substitution principle.

  2. This statement is inaccurate as indicated by the TS compiler, yet it remains consistent with the substitution principle. By stating that a variable originally declared as string can now be either string or number, you are essentially suggesting an extension of string to number. However, since number does not inherit from string, this would not be correct according to the principles of substitution. Nevertheless, the following implementation is valid:

class A {
    value!: number;
}

class B extends A {
    declare value: number & string;
}

In the above scenario, the value within class B is necessitated to encompass all properties of

number</code required by class <code>A
, then it adds the string attributes on top. It may prove challenging, however, to define a value that simultaneously represents both a string and a number.

Explaining the substitution principle with union types can be convoluted, especially considering its origins in languages lacking support for union types such as Java. Essentially, the principle underscores the ability to perform the following operations:

class A {}

class B extends A {}

class C {
    value!: A;
}

const x = new C();
x.value = new A();
x.value = new B();

And these operations remain valid.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Prevent clicking outside the bootstrap modal in Angular 4 from closing the modal

Just starting out with angular4 and incorporating bootstrap modal into my project. I want the modal to close when clicking outside of it. Here's the code snippet: //in html <div bsModal #noticeModal="bs-modal" class="modal fade" tabindex="-1" rol ...

The Angular code is currently unable to retrieve the quiz ID

Hey there, I am encountering an issue with fetching the Qid from the server side. Interestingly enough, it works perfectly fine in Postman but not in Angular. The problem seems to be isolated to the Quiz ID retrieval, as other IDs like Category ID and Ques ...

What is the process for importing a map from an external JSON file?

I have a JSON file with the following configuration data: { "config1": { //this is like a map "a": [ "string1", "string2"], "b": [ "string1", "string2"] } } Previously, before transitioning to TypeScript, the code below worked: import ...

Having difficulty selecting an item from the MaterialUI package

While trying to utilize the MaterialUI Select component with typescript/reactjs, I encountered an issue during the instantiation of the Select element. The error message I'm receiving states: Type 'Courses' is missing the following properti ...

The undefined property of MikroORM is not being returned

After experimenting with MikroORM, I've encountered a situation that has left me perplexed. Here is the snippet of code in question: const user = await appCtx.em.findOne(User, {email, accessToken}) if (!user) { return } conso ...

Advantages of Using Properties Over Getters and Setters Inside an Object's Class

Let's consider a scenario where we have a class A with another class B as its property. When class A needs to make modifications to class B, the question arises - is it preferable to use the getter method for these modifications or directly access th ...

Receiving an error while passing properties to a React component: "Property 'firstName' is not found on type 'Readonly<{}>'."

As a beginner in React, I need some patience I'm attempting to create a simple component where users can input their first and last names, update the values, and see them displayed after clicking a button. However, I keep encountering TypeScript comp ...

Is there a TypeScript alternative to triggering a click event on a specific class using $(".class").click()?

I am currently utilizing a date range picker within an Angular project. <button type="button" class="btn btn-danger daterange-ranges"> <i class="icon-calendar22 position-left"></i> <span></span> <b class="caret"></b ...

The FormControl is currently presenting ",required(control)" within its value field

Upon loading my form, the default values in the input fields are set to: ,required(control) { return isEmptyInputValue(control.value) ? { 'required': true } : null; } The template structure of my form is as follows: <form [formG ...

Tips for modifying query parameter serialization with nestjs/swagger?

After updating the nestjs/swagger package in my project to version ^4.0.0, I noticed a change in how Swagger serializes query parameters. Previously, it was done like this: /resources?parameter=1,2,3 But now it looks like this: /resources?parameter=1&am ...

TSConfig - Automatically Generates a ".js" File for Every ".ts" File

Having a unique software application with an unconventional file structure project ├── tsconfig.json ├── app_1 │ ├── ts │ └── js | └── app_2 ├── ts └── js I aim to compile files located inside the ...

performing the role of an ArrayList of object type through the utilization of a foreach loop

I am attempting to utilize a function from the main code that is part of the Category class. For instance: ArrayList<Category> categories = new ArrayList<>(); // The ArrayList has been populated with objects. for (Category category : categories) ...

The error "Property 'user' does not exist on type 'Session'." occurred while attempting to pass session data using express-session and accessing req.session.user

I'm currently working on creating a basic login form for users to access a website, where I plan to store their session data in a session cookie. The express-session documentation provides the following example for setting it up: app.post('/login ...

What is the best way to mock imports in NestJS testing?

I am interested in writing a unit test for my nestjs 'Course' repository service, which has dependencies on Mongoose Model and Redis. courses.repository.ts: import { Injectable, HttpException, NotFoundException } from "@nestjs/common"; ...

Creating formGroups dynamically for each object in an array and then updating the values with the object data

What I am aiming to accomplish: My goal is to dynamically generate a new formGroup for each recipe received from the backend (stored in this.selectedRecipe.ingredients) and then update the value of each formControl within the newly created formGroup with t ...

A guide to accessing the value of a web.config appsetting key in a Type Script service class

I am currently working on an Angular 2 application in Visual Studio 2015. In my project, I need to retrieve the app setting value from web.config in a TypeScript file (service.ts). Despite searching online and reading various resources, I have not been ab ...

Every time I click a button that triggers this.updateFn(...), I constantly encounter the error message indicating that this.updateFn(...) is not a function

In my current Angular project, I am attempting to call the following function: updateFn: () => (val: boolean) => void; I have tried invoking it like so: this.updateFn()(true); However, I consistently receive a this.updateFn(...) is not a function ...

Update the typing of a React component based on a specified prop

Currently, I am in the process of developing a React component called text that will have the ability to change the tag that is rendered. export interface TextProps extends HTMLAttributes<HTMLElement> { children: ReactNode; as?: React.ElementType ...

How can we effectively divide NGXS state into manageable sections while still allowing them to interact seamlessly with one another?

Background of the inquiry: I am in the process of developing a web assistant for the popular party game Mafia, and my objective is to store each individual game using NGXS. The GitLab repository for this project can be found here. The game includes the f ...

What is the best approach for handling errors in a NestJS service?

const movieData = await this.movieService.getOne(movie_id); if(!movieData){ throw new Error( JSON.stringify({ message:'Error: Movie not found', status:'404' }) ); } const rating = await this.ratingRepository.find( ...