Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Mutable type (opposite of Readonly) to lib.d.ts #24509

Open
4 tasks done
selmanj opened this issue May 30, 2018 · 23 comments · May be fixed by #49855 or #50451
Open
4 tasks done

Add a Mutable type (opposite of Readonly) to lib.d.ts #24509

selmanj opened this issue May 30, 2018 · 23 comments · May be fixed by #49855 or #50451
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript

Comments

@selmanj
Copy link

selmanj commented May 30, 2018

Search Terms

mutable, mutable type

Suggestion

lib.d.ts contains a Readonly type, but not a Mutable type. This type would be the opposite of Readonly, defined as:

type Mutable<T> = {
  -readonly[P in keyof T]: T[P]
};

Use Cases

This is useful as a lightweight builder for a type with readonly properties (especially useful when some of the properties are optional and conditional logic is needed before assignment).

Examples

 type Mutable<T> = {-readonly[P in keyof T]: T[P]};
 
 interface Foobar {
   readonly a: number;
   readonly b: number;
   readonly x?: string;
 }

 function newFoobar(baz: string): Foobar {
   const foobar: Mutable<Foobar> = {a: 1, b: 2};
   if (shouldHaveAnX(baz)) {
     foobar.x = 'someValue';
   }

   return foobar;
 }

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@mhegazy mhegazy added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels May 31, 2018
@ghost
Copy link

ghost commented May 31, 2018

Readonly modifiers "are a joke" (#13002) so this would still compile:

interface I { readonly x: number; readonly y: number; }
type Mutable<T> = {
    -readonly[P in keyof T]: T[P]
};
declare const i: I;
const j: Mutable<I> = i;

@selmanj
Copy link
Author

selmanj commented May 31, 2018

@andy-ms I can see why that would be undesired. Ideally you wouldn't be able to assign i to j here because Mutable<I> strips the readonly modifiers off i, which has been declared to be an I. Note that in my example, we are using Mutable<Foobar> as a shorthand to create a new type that we can freely mutate before returning it as a Foobar. The object instance assigned to variable foobar is never declared to be a type with readonly modifiers.

I don't want to discuss whether or not readonly modifiers are a joke. The language already provides a way to declare a type that removes readonly modifiers. This request is merely asking for a predefined type called Mutable available in typescript rather than having to define your own.

@FDIM
Copy link

FDIM commented Jan 9, 2019

I'd really like to see this as part of typescript, it is quite useful in unit tests.
We have a copy of this as Writable locally in the project and I also see that angular team has added Readwrite type in their devkit core package (not sure if it is exposed though)

On the other hand I've encountered an issue and I think this type should be

type Mutable<T> = T & {
  -readonly[P in keyof T]: T[P]
}; 

but this does not remove readonly flag and without this you are not able to pass Mutable to a function that accepts Foo if your object has private properties

@jdom
Copy link
Member

jdom commented Feb 6, 2019

I have something similar in my codebase, but that covers ReadonlyArray<string> too:

type MutableReadonlyStringArrays<T> = { [P in keyof T]: T[P] extends ReadonlyArray<string> ? string[] : T[P] };
type Mutable<T> = MutableReadonlyStringArrays<{ -readonly [P in keyof T]: T[P] }>;

This can probably be generalized to cover other non-string arrays too, I just didn't have the need in my usage. But thought it would be nice to cover these if it gets into lib.d.ts

@jdom
Copy link
Member

jdom commented Feb 19, 2019

I actually had to generalize this, and ended up with the following:

type Mutable<T> = { -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U> ? U[] : T[P] };

This will make properties of a type mutable, plus also turn properties that are ReadonlyArray<U> as normal arrays of U too. Nevertheless, is there a way to make this recursive too so it applies to nested hierarchies?

@jdaley
Copy link

jdaley commented Mar 12, 2019

@jdom

type Mutable<T> = {
    -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U> ? Mutable<U>[] : Mutable<T[P]>
};

@lirbank
Copy link

lirbank commented Nov 15, 2019

A DeepMutable would be nice too.

type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> };

See https://stackoverflow.com/a/43001581/1959584

@baetheus
Copy link

baetheus commented Jan 15, 2020

I wish the typescript designers had gone with immutability by default instead of mutability. If all properties and variable declarations were readonly by default then I wouldn't have to type readonly so often. Mutable properties could have used a keyword like mutable. It would be much easier to find bugs, just grep -r mutable *.ts for the likely culprits... Or, at the very least, a compiler option to default to readonly instead of mutable would be useful for many projects.

@PreventRage
Copy link

I wish the typescript designers had gone with immutability by default instead of mutability. If all properties and variable declarations were readonly by default then I wouldn't have to type readonly so often. Mutable properties could have used a keyword like mutable. It would be much easier to find bugs, just grep -r mutable *.ts for the likely culprits... Or, at the very least, a compiler option to default to readonly instead of mutable would be useful for many projects.

I suspect that would severely break the "JavaScript is valid Typescript" design point

@baetheus
Copy link

@PreventRage How so? The mutable keyword doesn't exist in javascript and a mutability flag in compiler config would cause compiler errors in the same way that the strict flag currently does.

@PreventRage
Copy link

@baetheus I think technically you have a point. Which I would restate as "The various strict* options already cause valid JS to be invalid TS, so why worry about adding other such?"

My sense is that apply more rigorous rules and force your JS to be cleaner (strict*) is qualitatively different from invert default behavior (immutableByDefault), and that the former exception follows the spirit of the design while the latter does not. But that's now just a sense/opinion on my part, not any kind of proof.

Strangely (perhaps I'm totally contradicting myself?) I find the notion of a way to mark an individual type as immutable (I think just a tidier version of the Readonly<> generic) feels consistent with the design while getting rid of most of the tediously repetitive readonly.

@baetheus
Copy link

baetheus commented Jan 18, 2020

@PreventRage Thanks for getting back to me! I understand that immutability is not for everyone, and I often find myself using mutable structures when doing hot path optimization. While I have no particular issues with the argument that immutableByDefault might not be in the spirit of the language as designed, it nevertheless would be useful for me (and probably a few other programmers that I've worked with over the years).

That said, I'm not in earnest expecting that such a feature would be implemented, I mainly meant to put forward a feature that would pair well with the proposed Mutable<T> type outlined in this issue and to gripe about my own feelings on TypeScript. (A good portion of the work I do is the "last 20%" of a project prior to release. Anecdotally, the misuse of mutability in JS and TS is the primary culprit for most project stalls.)

As for using the Readonly<T> generic to achieve immutability, you are right and it is a solution that I've used regularly. My only qualms are that the burden of keeping track of what is immutable is left to the programmer, and not the compiler. Any opportunity of getting the compiler to do more of my job is an opportunity worth taking.

@dhoulb
Copy link

dhoulb commented May 10, 2020

I just came across the following minor use case for this:

I'm creating a class with public properties I have marked as readonly, but I want to be able to write to those properties within the class.

The Mutable type provides an understandable way to describe it. The code's tidy enough that I don't mind the type casting:

class MyClass {
    readonly myVal: string;

    set(myVal: string) {
        (this as Mutable<this>).myVal = myVal;
    }
}

In general I think it's a good idea to add this helper type just for parity with Readonly

@simon-robertson
Copy link

Sorry for bumping an old issue, but I just wanted to say that I too have had to declare my own Mutable type occasionally in projects. Obviously this is easy enough to do, but having it as a standard type would make sense

declare type Mutable<T extends object> = {
    -readonly [K in keyof T]: T[K]
}

I also typically declare a Struct type to construct generic objects, mainly to provide a property key type to TSC, but also to give the property values a type

declare type Struct<T> = {
    [key: string]: T
}

A standard Mutable type definitely gets my vote though

@sushruth
Copy link

Adding to this here - Mutable standard type helper would be greatly useful for apollo/graphql users.

@rosedo
Copy link

rosedo commented Aug 10, 2020

Maybe this can help, although it requires a call to a function

function fn<T>(readonlyArray: readonly T[]): T[]

@castarco
Copy link

castarco commented Mar 5, 2021

I recently commented on a similar topic... what about introducing a mutable qualifier keyword instead of having to rely on generic types for that?

For more context, although we know that objects are mutable by default in JS and TS, in TS they can be qualified as "unknown" too (not just "readonly" or "mutable")... and the default qualifier is "unknown", not "mutable". That's why it's possible to do some unsafe assignments without any complaint from the type checker ( #14150 ).

The generic Mutable type wouldn't be able to help in this case.

My previous comment:
#14150 (comment)

In the linked comment I also refer to an old issue that was closed without further public discussion (the conversation seems to be available only for the development team).

@ediblecode
Copy link

For anyone reading this and looking for Mutable: it may not be 'built in', and you could declare your own implementation as suggested here. However there's a Mutable definition in type-fest which is as good as any, is a well-written and maintained library. So probably worth considering that if you need a Mutable definition.

@simeyla
Copy link

simeyla commented Jan 24, 2022

I keep running into this issue the more I use const, which is a very powerful way to retain strong typing in static data.

    const animals = ['dog', 'cat', 'mouse'] as const;     // type is  readonly ["dog", "cat", "mouse"]

However if you try to assign this to something 'wider' then you get tripped up by its ReadOnly nature.

The type 'readonly ["dog", "cat", "mouse"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

    const animals = ['dog', 'cat', 'mouse'] as const;
    const farm: string[] = animals;

This intuitively just doesn't feel right (even though I understand why):

I don't even necessarily want farm to be read only so with respect to the idea of a Mutable type I'm not sure how that would even fit in here. Maybe the best solution here is just to make a copy?

const farm = [...animals];

@chharvey
Copy link

@simeyla You are correct; the way to handle this is to make a copy of animals, either by spreading, using .slice(), or something similar. Your example isn’t really a case for needing a generic Mutable type.

It actually enforces type safety to disallow assigning animals to farm, since farm is already mutable. In general, readonly T[] should not be assignable to T[] (which is mutable). To see why, consider the following.

Suppose someone could do this:

const animals = ['dog', 'cat', 'mouse'] as const; // type is readonly ["dog", "cat", "mouse"]
// @ts-expect-error
const farm: string[] = animals;

then:

farm.push('chicken');

now inspect animals:

animals; // type is readonly ["dog", "cat", "mouse"]
console.log(animals);
//> ["dog", "cat", "mouse", "chicken"]

The immutable object has been mutated!

Your solution is correct. You must make a copy:

const farm: string[] = [...animals]; // or `animals.slice()`, etc...

@graphemecluster
Copy link
Contributor

graphemecluster commented Jul 4, 2022

I need this! Currently both Partial and Required present and I can't understand why an opposite to Readonly doesn't. Although I noticed @RyanCavanaugh did mention that the Team decided not to include utility type aliases, this is one of the four basic mapping types in TypeScript. Without this I feel TypeScript is literally “Partial”, or incomplete. There are already countless use cases, and I don't think including a library or defining it myself just for this single type is a good idea.

unless they're required for declaration emit purposes

I found that this can and should be used in the definition of Object.assign in the lib for correctness

@graphemecluster graphemecluster linked a pull request Jul 10, 2022 that will close this issue
@julian-kingman-lark
Copy link

My use-case is that I have a class with mutable properties in a react-based app, which can be instantiated with properties stored in state. In react, there are immutable values everywhere (e.g. state), so ideally I would like typescript to give me errors if I try to pass an immutable value to a mutable one. I'm currently pulling out my hair hunting for a rogue immutable value that's getting set to a mutable class property, rendering it immutable, and thus useless...

// ideally I'd be able to say something like:
class UtilityClass {
  public mutableProperty: Mutable<Record<string, string>>
  constructor(prop) {
    this.mutableProperty = prop;
  }
  // ...
}

// and then this would happen
const Component = () => {
  const [immutableState, setImmutableState] = useState({a: 1});
  // Type Error: Can't set 'UtilityClass' mutable property 'mutableProperty' to immutable property 'immutableState'
  const classInstance = new UtilityClass(immutableState);
  classInstance.mutableProperty.a = 2; // this doesn't work b/c we made it immutable, thankfully TS warned us
  // ...
}

@c-vetter
Copy link

@castarco
I'm with you, mutable would be great.

However, that's kind of tangential to the present issue. I think, if mutable was available, it would relate to Mutable as readonly relates to Readonly, so it would be nice to have both. However, Mutable can already be implemented with the current features, while mutable will probably be far more involved.

@julian-kingman-lark
While I feel your pain, I think your comment is more strongly related to wanting declarative mutability and stricter handling of readonly. Nonetheless, I'll address it as well as I can.

I think your issue is unrelated to React itself, so I'm rewriting your sample here to make it self-contained:

class UtilityClass {
	constructor (public mutableProperty: Record<string, string>) {}
}

export const Component = () => {
	const immutableState = {a: `1`} as const
	const classInstance = new UtilityClass(immutableState) // Type Error with #58296
	classInstance.mutableProperty.a = `2` // will never error
}

The call to new UtilityClass(immutableState) should error when #58296 lands and you activate the new option, because the assignment of a const/readonly/immutable value to a mutable parameter is illegal. Note that this does not require a Mutable utility because Record is already mutable.

The assignment mutableProperty.a = "2", however will never error. That is because for TS to consider that an error, it would need to change the type of classInstance such that it wouldn't adhere to UtilityClass's interface anymore. And that would open an unmanageable abyss, because now TS would need to pretty much understand your whole app, at which point we're not talking about a type system but some kind of run-in-advance system. Would be awesome, but it's unfeasible.

Your original public mutableProperty: Mutable<Record<string, string>> would make sense in the context of adding mutable as @castarco is talking about, in contrast to the two current states of readonly and unknown (which is distinct from the unknown type). See “Readonly properties” @ #6614 about that.

@simon-robertson
[OT] Your Struct<T> is exactly tslib's Record<string,T>. Especially in light of your wanting to narrow the key type, Record seems to be your friend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Projects
None yet