How to exclude getter only properties from type in typescript
Asked Answered
G

3

35

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

Ginny answered 21/9, 2018 at 11:57 Comment(0)
H
55

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];
    }
}
Handsaw answered 24/9, 2018 at 5:26 Comment(9)
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.Esdras
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).Handsaw
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 :)Esdras
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?Parker
If you would rather use a library, check out npmjs.com/package/ts-essentialsHaematozoon
Alternative 2 doesn't seem to be equivalent - see here.Tobey
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: github.com/pirix-gh/ts-toolbelt/issues/97Systematic
@JustinAnyhowStep - That GitHub issue is marked as fixed. Is your comment still valid?Replicate
@Replicate I think it's valid, since the fixed version is more complex than this anwser: github.com/millsp/ts-toolbelt/commit/…Icefall
E
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
})
Esdras answered 21/9, 2018 at 12:26 Comment(1)
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?Keikokeil
W
4

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

Withrow answered 19/3, 2020 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.