31

Getters in the class are readonly properties so throwing type error from following code make sense.

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

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

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

example in playground

Eduard Jacko
  • 1,974
  • 1
  • 16
  • 29

3 Answers3

51

While readonly does not directly affect whether types are assignable, it does affect whether they are identical. To test whether two types are identical, we can abuse either (1) the assignability rule for conditional types, which requires that the types after extends be identical, or (2) the inference process for intersection types, which throws out identical types from both sides. Then we just use mapped types as in Titian Cernicova-Dragomir's answer to look at each property of Car in turn and see whether it is identical to a mutable version of itself.

// 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];
    }
}
Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • 7
    This answer uses the fact that the compiler will consider the two generic functions identical only if X and Y (which are only used in the constraint for `T`)and identical (including their `readonly` attributes), while this works it does seem a bit inconsistent, do we know (ie have some assurance form the compiler team) this will not break in future compiler releases. – Titian Cernicova-Dragomir Oct 23 '18 at 13:16
  • 1
    No assurance, but I think the behavior is pretty unlikely to change. If it does, I added another possible approach (which could also break, but having two options is better than having one). – Matt McCutchen Oct 23 '18 at 13:55
  • 1
    Fair enough, I just wanted to know, sometimes implementation specific behavior is sanctioned by someone on the team (for example i'm sure I read somewhere Ryan Cavanaugh saying the ` T & {}` to decrease the priority of the inference site could be relied upon). But if we use it enough it becomes harder to change :) – Titian Cernicova-Dragomir Oct 23 '18 at 13:59
  • 1
    Wonderful @MattMcCutchen! I dare you now to create a recursive version of `WritablePart` :D In the spirit of `DeepReadonly` and `DeepPartial`. Do you think it would be possible? – DanielM Dec 06 '18 at 19:58
  • 6
    If you would rather use a library, check out https://www.npmjs.com/package/ts-essentials – Krzysztof Kaczor Jun 17 '19 at 07:11
  • 1
    Alternative 2 doesn't seem to be equivalent - see [here](https://www.typescriptlang.org/play/#code/C4TwDgpgBAggjFAvFA3lAhgLigZ2AJwEsA7AcygF8BYAKFElgCYlUoAjbYgVwFs2J8lWrXrQAkgDMAogEcu6ADY44AHgAaAGigBNLTC0AhAHxJaUc1AAUKgCpHLASiQmbUCAA9gEYgBMcUNSgAfigEbEYnDy9fHDMLaztHZyhXKO8-HWDQqHCnEJgcqAMAbmE6cHFpOUUcRnUtXVhDE0Q48wBtRgBdKAAyKHaABi04LTUetJiB7r6B4dCGnv6hrRIJASgAdS0uYgBrYgB7AHdiLraszbdPdP8EEINCmAvsErLRFIRkSVl5JVV4LMYIw9KMoAAiEAQHDgrTgo7gkwAeiRUCOVlIh0OPgcIgqKWY3yqf1qKkB-WBoLhUJhcIRyNRNOKUGAAAtCP4OVBjvhDmQgA). – ford04 Jan 12 '20 at 15:24
  • For anyone who comes here in the future, I'd like to point out that the answer doesn't always work and can be easily broken: https://github.com/pirix-gh/ts-toolbelt/issues/97 – Justin AnyhowStep Mar 10 '20 at 07:16
  • @JustinAnyhowStep - That GitHub issue is marked as fixed. Is your comment still valid? – charles-allen May 16 '20 at 08:57
  • @charles-allen I think it's valid, since the fixed version is more complex than this anwser: https://github.com/millsp/ts-toolbelt/commit/91aef20a67da686fda397051f467b00a43db6591 – Yao Zhao Aug 09 '21 at 16:45
8

Edit See @matt-mccutchen for an intresting workaround to this issue.

Original answer

readonly is a rather weak modifier in that is does not impact assignability. So for example you can assign an object with readonly properties to one with those same mutable properties and the compiler will not complain:

let roCar: Partial<Car> = { hp: 10 } // we can assign a  mutable object to a referecne with a readonly property
roCar.hp = 10; // error hp is readonly

//But we can also assign an object with a readonly property to a fully mutable version of it 
let allMutableCar: { -readonly [P in keyof Car]: Car[P] } = new Car();
allMutableCar.hp = 10; // No compile time error

This is a known issue, documented here.

Because of this assignability rule there is no way to distinguish in conditional types the difference between a readonly field and a mutable one.

One workaround is to add something extra to the type of readonly fields. This will not impact how you can use the field but it will give us a hook to remove the key.

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
})
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • I'm have an issue with this were I would like to have a getter return null, however `null & readonly` does not work, just becomes `readonly`. Is there a work-around for this? – ThomasReggi Jan 14 '20 at 06:11
3

Hey my question might have an answer to yours.

How do you get the type of the object that is cloned from a Class Instance?

Basically you can exclude all the getters (and functions) by doing this

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; 

then your function would work like this:

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

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

My question Asks how to get the type CarNoGetters without using Javascript, ie. var snapShot = {...new Car()};

but if you don't care you can use that.

(note I use TS ^3.75)

ts playground

lonewarrior556
  • 3,917
  • 2
  • 26
  • 55