Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript allows passing Readonly<T> to a mutating function

Tags:

typescript

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)) 
}
like image 485
Damask Avatar asked Sep 14 '25 12:09

Damask


2 Answers

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;
}
like image 58
Yash Parekh Avatar answered Sep 17 '25 04:09

Yash Parekh


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
  }


}
like image 38
Alexander Nenashev Avatar answered Sep 17 '25 03:09

Alexander Nenashev