Instructions for excluding readonly properties from a type in typescript

Class getters are readonly properties, so it's logical that the code below would throw a type error.

class Car {
    engine: number;
    get hp() {
        return this.engine / 2;
    }
    get kw() {
        return this.engine * 2;
    }
}

function applySnapshot(
    car: Car,
    snapshot: Partial<Car> // <-- how to exclude readonly properties?
) {
    for (const key in snapshot) {
        if (!snapshot.hasOwnProperty(key)) continue;
        car[key as keyof Car] = snapshot[key as keyof Car];
        // Cannot assign to 'hp' because it is a constant or a read-only property.
    }
}

Is there a way to cast only writable properties to a specific type and exclude all getters?

Check out an example on TypeScript playground

Answer №1

When it comes to determining whether two types are identical, the usage of the `readonly` keyword may not directly impact their assignability, but it does play a role in establishing their identity. One approach to test type identity involves exploiting either (1) the assignability rule for conditional types, which mandates that types following `extends` must be identical, or (2) the inference mechanism for intersection types, which eliminates identical types from both ends. By utilizing mapped types, as demonstrated in Titian Cernicova-Dragomir's response, each property of `Car` can be examined individually to ascertain if it matches a mutable version of itself.

// Link: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
type IfEquals<X, Y, A, B> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? A : B;

// Alternatively:
/*
type IfEquals<X, Y, A, B> =
    [2] & [0, 1, X] extends [2] & [0, 1, Y] & [0, infer W, unknown]
    ? W extends 1 ? B : A
    : B;
*/

type WritableKeysOf<T> = {
    [P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>
}[keyof T];
type WritablePart<T> = Pick<T, WritableKeysOf<T>>;

class Car {
    engine: number;
    get hp() {
        return this.engine / 2;
    }
    get kw() {
        return this.engine * 2;
    }
}

function applySnapshot(
    car: Car,
    snapshoot: Partial<WritablePart<Car>>
) {
    let key: keyof typeof snapshoot;
    for (key in snapshoot) {
        if (!snapshoot.hasOwnProperty(key)) continue;
        car[key] = snapshoot[key];
    }
}

Answer №2

Update Refer to @matt-mccutchen for a unique solution to this particular issue.

Initial response

readonly doesn't restrict assignability, allowing mutable properties to be assigned to objects with readonly properties without triggering any compiler errors:

let roCar: Partial<Car> = { hp: 10 } // assigning a mutable object to a reference with a readonly property is permitted
roCar.hp = 10; // results in an error stating that hp is readonly

// However, it's also possible to assign an object with a readonly property to a completely mutable version of it 
let allMutableCar: { -readonly [P in keyof Car]: Car[P] } = new Car();
allMutableCar.hp = 10; // No compile time error

This flaw has been recorded here.

Due to this assignability rule, distinguishing between a readonly field and a mutable one in conditional types becomes challenging.

A workaround involves augmenting the type of readonly fields with additional information. While not affecting the field's usability, this gives us a means to remove the key when necessary.

type readonly = { readonly?: undefined };
class Car {
    engine!: number;
    get hp() : number & readonly {
        return this.engine / 2;
    }
    get kw() : number & readonly {
        return this.engine * 2;
    }
}

type NoReadonlyKeys<T> = { [P in keyof T]: 'readonly' extends keyof T[P] ? never : P }[keyof T]

type PartialNoReadonly<T> = Partial<Pick<T, NoReadonlyKeys<T>>>  
type Mutable<T> = { -readonly [P in keyof T]: T[P] }
function applySnapshot(
    car: Car,
    snapshoot: PartialNoReadonly<Car>
) {
    const mutableCar: Mutable<Car> = car; // erase readonly so we can mutate
    for (const key in snapshoot) {
        let typedKey = key as keyof typeof snapshoot
        if (!snapshoot.hasOwnProperty(key)) continue;
        mutableCar[typedKey] = snapshoot[typedKey] as any;
    }
}

applySnapshot(new Car(), {
    engine: 0
})
applySnapshot(new Car(), {
    hp: 0 /// error
})

Answer №3

Hey there might be an answer to your question here.

How can you determine the type of object cloned from a Class Instance?

Essentially, you can exclude all the getters (and functions) by following this method:

class Car {
    engine: number = 1;
    get hp() {
        return this.engine / 2;
    }
    get kw() {
        return this.engine * 2;
    }
}

var snapShot = {...new Car()};
type CarNoGetters = typeof snapShot; 

Afterwards, your function would operate as follows:

function applySnapshot(
    car: Car,
    snapshot: CarNoGetters
) {

    for (const key of Object.keys(snapshot) as Array<keyof typeof snapshot>) {
        car[key] = snapshot[key];
    }
}

The question posed is how to obtain the type CarNoGetters without using Javascript, like in var snapShot = {...new Car()};

However, if that doesn't matter to you, feel free to use it.

(note: TypeScript version ^3.75)

ts playground

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

Exploring the inner components of an entity without the need for external tools

I am currently enhancing TypeScript usage in a project by implementing generics. The challenge I am facing involves dealing with a complex object retrieved from the backend, which consists of a class with numerous attributes, most of which are classes them ...

Combining virtual scrolling with pagination in PrimeNG componentsCombining virtual scrolling and pagination is

I am currently facing a situation where I have to display 150 records simultaneously in my primeng data grid. However, the grid starts freezing after loading just 50 records. This is because there are 18 columns in my grid, causing it to consume a lot of ...

What could be causing the Angular/TypeScript model that I imported to fail to properly map to my service within my Angular application?

Struggling with implementing a system for manual entry of mutual fund information, I am running into errors when trying to read properties from the 'fund' model in my application. The Developer Tools console is displaying the following error mess ...

Exploring the integration of React.Components with apollo-client and TypeScript

I am in the process of creating a basic React component using apollo-client alongside TypeScript. This particular component is responsible for fetching a list of articles and displaying them. Here's the code: import * as React from 'react' ...

Angular Form Validations - input values must not match the initial values

These are my current reactive form validations: ngOnInit(): void { this.userForm = this.formBuilder.group({ status: {checked: this.selectedUser.status == 1}, username: [this.selectedUser.username, [Validators.required, Validators.minLeng ...

The parameter type ‘DocumentData’ cannot be assigned to type ‘never’ in this argument

I've been struggling to find a solution to my issue: Ts gives me an error: Argument of type 'DocumentData' is not assignable to parameter of type 'never' I attempted the solution I found on this page: Argument of type 'Docume ...

Exploring abstract classes for diverse implementation strategies?

Consider the following scenario: export abstract class Button { constructor(public config: IButton) {} abstract click(); } Now, we have a concrete class: class ButtonShowMap extends Button { private isShow = false; constructor(public config: IBu ...

"Using Angular and TypeScript to dynamically show or hide tabs based on the selected language on a website

When switching the language on the website, I want to display or hide a specific tab. If the language is set to German, then show the tab; if any other language is selected, hide it. Here's my code: ngOnInit(): void { this.translate.onLangChange.s ...

Setting up the Leaflet routing feature on an Ionic 2 app

Recently, I set up a basic Ionic 2 project that integrates leaflet with the help of this repository: https://github.com/SBejga/ionic2-map-leaflet. Following that, I ran the command: sudo npm install --save leaflet-routing-machine to add leaflet routing m ...

Obtain the Enum's Name in TypeScript as a String

I am currently looking for a solution to transform the name of an enum into a string format. Suppose I have the following Response enum, how can I obtain or convert 'Response' into a string? One of my functions accepts any enum as input and requi ...

Posting forms in NextJS can be optimized by utilizing onChange and keypress events for input fields

I am currently working on my first Edit/Update form within a newly created NextJs application as I am in the process of learning the framework. I seem to be facing an issue where the form constantly posts back to the server and causes the page to refresh ...

I'm looking to receive the specific data types for express request arguments. How can I

Currently, I am working on incorporating authentication into an express application using passport and typescript. For defining the user model, I have utilized typegoose. Once the login request passes through the passport strategy, a login method is called ...

What methods can TypeScript employ to comprehend this situation?

There's an interesting scenario when it comes to assigning a variable of type unknown to another variable. TypeScript requires us to perform type checking on the unknown variable, but how does TypeScript handle this specific situation? It appears that ...

The webpage is currently experiencing difficulty in displaying any information

As a beginner in React and typescript, I am working on a simple application that fetches data from an API and displays it on a web page. Despite fixing some errors and seeing the data in the console log, I am struggling to display any data on the actual we ...

Angular2's integration of backend API calls

My backend calls are functioning correctly, but I'm encountering an issue with promises. I am unable to retrieve the data from the first promise in order to make the second call. Any insights on where I might be going wrong? login() { if (thi ...

Slow auto page refresh in local development for Angular2 on Windows can be quite frustrating

Recently diving into angular2 and following the heros tutorial from docs. Struggling with a sluggish development experience while working with angular2. It's taking approximately 5 seconds for angular2 to recognize changes in files, followed by anothe ...

Tips for preventing microphone from muting when the screen is locked in an Angular PWA during voice calls

After successfully implementing a PWA in Angular with agora.io voice calling, I encountered an issue where the microphone would get muted on both iOS and Android devices when the phone was locked. Even after unlocking the phone, the microphone remained mut ...

Removing a dynamic component in Angular

Utilizing Angular dynamic components, I have successfully implemented a system to display toaster notifications through the creation of dynamic components. To achieve this, I have utilized the following: - ComponentFactoryResolve - EmbeddedViewRef - Ap ...

Error: Idle provider not found in the promise

Currently, I am integrating ng2-idle into an AngularJS 2 application. After successfully including the ng2-idle package in the node_modules directory of my project, I attempted to import it into one of my components as shown below: Dashboard.component.ts: ...

Directive for creating a custom loading indicator in Angular

I have created a custom Angular element directive that displays and hides a loading indicator based on a condition from a service call. The directive is used as an element within another element. While the directive itself works correctly, the issue is tha ...