Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable allowing assigning Readonly types to non-readonly types

I've been looking at the readonly type in typescript. Sadly, it does not work as i hope it would. For instance, see the code below:

interface User{
    firstName: string;
    lastName: string;
}

const user: Readonly<User> = {
    firstName: "Joe",
    lastName: "Bob",
};

const mutableUser: User = user; //Is it possible to disallow this?

user.firstName = "Foo" //Throws error as expected
mutableUser.firstName ="Bar"//This works

Is it somehow possible to use the readonly type in a way that doesn't allow assigning it to another type that is not readonly? If it isn't can i solve it in any other way?

like image 643
Erik Johansson Avatar asked Nov 21 '18 13:11

Erik Johansson


1 Answers

Ah, you've run into a problem that annoyed someone enough to file an issue with the memorable title "readonly modifiers are a joke" (which has since changed to something more neutral). The issue is being tracked at Microsoft/TypeScript#13347, but there doesn't seem to be much movement on it. For now, we just have to deal with the fact that readonly properties don't affect assignability.

So, what workarounds are possible?


The cleanest is to give up on readonly properties and instead use some kind of mapping that turns an object into something you really can only read from, via something like getter functions. For example, if readonly properties are replaced with functions that return the desired value:

function readonly<T extends object>(x: T): { readonly [K in keyof T]: () => T[K] } {
  const ret = {} as { [K in keyof T]: () => T[K] };
  (Object.keys(x) as Array<keyof T>).forEach(k => ret[k] = () => x[k]);
  return ret;
}

const user = readonly({
  firstName: "Joe",
  lastName: "Bob",
});

const mutableUser: User = user; // error, user is wrong shape

// reading from a readonly thing is a bit annoying
const firstName = user.firstName();
const lastName = user.lastName();

// but you can't write to it
user.firstName = "Foo" // doesn't even make sense, "Foo" is not a function
user.firstName = () => "Foo" // doesn't work because readonly

Or similarly, if a readonly object only exposes a single getter function:

function readonly<T extends object>(x: T): { get<K extends keyof T>(k: K): T[K] } {
  return { get<K extends keyof T>(k: K) { return x[k] } };
}

const user = readonly({
  firstName: "Joe",
  lastName: "Bob",
});

const mutableUser: User = user; // error, user is wrong shape

// reading from a readonly thing is a bit annoying
const firstName = user.get("firstName");
const lastName = user.get("lastName");

// but you can't write to it
user.firstName = "Foo" // doesn't even make sense, firstName not a property

It's annoying to use, but definitely enforces the spirit of readonliness (readonlity? 🤷‍♂️) and you can't accidentally write to something readonly.


Another workaround is to run a helper function which will only accept mutable values, as @TitianCernicova-Dragomir has suggested. Possibly, like this:

type IfEquals<T, U, Y = unknown, N = never> =
  (<V>() => V extends T ? 1 : 2) extends
  (<V>() => V extends U ? 1 : 2) ? Y : N;
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type IsMutable<T, Y=unknown, N=never> = IfEquals<T, Mutable<T>, Y, N>

const readonly = <T>(x: T): Readonly<T> => x;
const mutable = <T>(
  x: T & IsMutable<T, unknown, ["OOPS", T, "has readonly properties"]>
): Mutable<T> => x;

const readonlyUser = readonly({
  firstName: "Joe",
  lastName: "Bob",
});
const mutableUser = mutable(
  { firstName: "Bob", lastName: "Joe" }
); // okay

const fails: User = mutable(readonlyUser); // error, can't turn readonly to mutable
// msg includes ["OOPS", Readonly<{ firstName: string; lastName: string; }>
// , "has readonly properties"]

const works = readonly(mutableUser); //okay, can turn mutable to readonly

Here the readonly function will accept any value of type T and return a Readonly<T>, but the mutable function will only accept values which are already mutable. You have to remember to call mutable() on any value you expect to be mutable. That's fairly error-prone, so I don't really recommend this method.


I also played around with the idea of making a fake Readonly<T> type which modified T in such a way as to distinguish it structurally from T, but it was as cumbersome as the getter-function method. The problem is that, assuming you want to be able to assign mutable values to readonly variables but you want to be prevented from assigning readonly values to mutable variables, the readonly modifier needs to widen the type of T, not narrow it. That limits the options to something like Readonly<T> = {[K in keyof T]: T[K] | Something} or Readonly<T> = T | Something. But in each case it becomes very difficult to actually read the readonly properties, since you have to narrow the types back. If you need boilerplate every time you read a property, you might as well use a getter function. So, forget that.


To wrap up: I think the getter function method is probably your best bet if you really want to enforce properties that cannot be written. Or maybe you should just give up on readonly modifiers, as they are, after all, a joke 🤡. Hope that helps. Good luck!

like image 93
jcalz Avatar answered Sep 24 '22 19:09

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!