Is there a way to enforce devs writing the code to use Readonly?
Say, we have a top level function declared with Readonly params and I want to force people to have nested functions be declared with the same Readonly signature.
Now it feels like, TS accepts Readonly version of a type as the same type. This example for instance compiles without hints/errors.
type F<T> = (t: Readonly<T>) => void;
type Params = { a: number };
function mutate(p: Params) {
p.a = 1;
return p;
}
const f: F<Params> = (params) => {
console.log(params.a);
console.log(mutate(params))
}
TypeScript’s Readonly is a compile-time constraint that prevents mutation of object properties through a specific reference. However, it’s not enforced at runtime and it’s not a unique type—it's structurally compatible with T. That means you can pass a Readonly where a T is expected, and vice versa, as long as the usage doesn’t violate the readonly contract within that context.
type F<T> = (t: Readonly<T>) => void;
type Params = { a: number };
function mutate(p: Params) {
p.a = 1;
return p;
}
const f: F<Params> = (params) => {
console.log(params.a);
console.log(mutate(params))
}
To make sure no one accidentally mutates data, you can
function mutate(p: Readonly<Params>) {
// p.a = 1; // Now a compile-time error
return p;
}
Parameter variance in TS is a kind of mess. A good video about variance: http://youtube.com/watch?v=EInunOVRsUU.
Seems in the case the parameter is bivariant.
I would try to incorporate "mutable" branded type, that way you could force using Readonly
(it's convoluted but makes some sense):
Playground
{
type F<T> = (t: Readonly<T>) => void;
type Params = Mutable<{ a: number }>;
type Mutable<T> = T & {__mutable: never};
type Readonly<T> = Omit<{ readonly [P in keyof T]: T[P]; }, '__mutable'>;
// a utility to easily create mutable values;
const mutable = <T,>(a: T) => a as Mutable<T>;
const params = {a: 1};
mutate(params); // ERROR
mutate2(params); // OK
const params2 = mutable({a: 1});
mutate(params2); // OK
mutate2(params2); // OK
function mutate(p: Params) {
p.a = 1;
return p;
}
function mutate2(p: Readonly<Params>) {
p.a = 1;
return p;
}
const f: F<Params> = (params) => {
console.log(params.a);
console.log(mutate(params)) // ERROR
console.log(mutate2(params)) // OK
}
}
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