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
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!
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 .
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.
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];
}
}
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
})
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With