Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to exclude getter only properties from type in typescript

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

like image 840
Eduard Jacko Avatar asked Sep 21 '18 11:09

Eduard Jacko


People also ask

How do you omit a property in TypeScript?

Use the Omit utility type to exclude a property from a type, e.g. type WithoutCountry = Omit<Person, 'country'> . The Omit utility type constructs a new type by removing the specified keys from the existing type. Copied!

How do I omit multiple keys in TypeScript?

To omit multiple keys from an object, pass a union of string literals in K . In the next example, we generate a Person type off of SoccerPlayer by removing the team and careerGoals .

What does omit type do in TypeScript?

TypeScript provides a number of utility types which are used to solve a particular problem that using types in Javascript creates. One very useful utility type used in TypeScript is the Omit type, which lets us customize an already existing type.


3 Answers

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];
    }
}
like image 70
Matt McCutchen Avatar answered Sep 21 '22 16:09

Matt McCutchen


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
})
like image 39
Titian Cernicova-Dragomir Avatar answered Sep 19 '22 16:09

Titian Cernicova-Dragomir


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

like image 24
lonewarrior556 Avatar answered Sep 18 '22 16:09

lonewarrior556