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

Suggestion: DeepReadonly<T> type #13923

Open
mprobst opened this issue Feb 7, 2017 · 54 comments
Open

Suggestion: DeepReadonly<T> type #13923

mprobst opened this issue Feb 7, 2017 · 54 comments
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript

Comments

@mprobst
Copy link
Contributor

mprobst commented Feb 7, 2017

TypeScript Version: 2.1.1 / nightly (2.2.0-dev.201xxxxx)

Code

It would be nice to have a shard, standard library type that allows to express deep readonly-ness (not really const, since methods are out of scope, but still...):

interface Y { a: number; }
interface X { y: Y; }
let x: Readonly<X> = {y: {a: 1}};
x.y.a = 2;  // Succeeds, which is expected, but it'd be nice to have a common way to express deep readonly

type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
}
let deepX: DeepReadonly<X> = {y: {a: 1}};
deepX.y.a = 2; // Fails as expected!
@felixfbecker
Copy link
Contributor

The same for Partial

@iRath96
Copy link

iRath96 commented Mar 4, 2017

Having a DeepReadonly<T> type would probably also allow for const methods (similar to how C++ does this).

class A {
  public x: number;
  unsafe() { // `this` is of type "A"
    this.x = 2;
  }
  const safe() { // "const" causes `this` to be of type "DeepReadonly<A>"
    console.log(this.x);
    // this.x = …; would yield a compiler error here
  }
}

let a: A;
a.unsafe(); // works fine, because "a" is of type "A"
a.safe(); // works fine, because "A" is a superset of "DeepReadonly<A>"

let readonlyA: DeepReadonly<A>;
a.safe(); // works fine, because "a" is of type "DeepReadonly<A>"
a.unsafe(); // would result in an error, because "DeepReadonly<A>" is not assignable to the required `this` type ("A")

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@mprobst
Copy link
Contributor Author

mprobst commented Jul 20, 2017

This has somewhat odd behaviour for callables, e.g. when calling set on a DeepReadonly<Map<...>>:

Cannot invoke an expression whose type lacks a call signature. Type 'DeepReadonly<(key: string, value?: number | undefined) => Map<string, number>>' has no compatible call signatures.

@ChuckJonas
Copy link

@mprobst I just ran into this issue using the same type... Any idea how to fix this?

@mhegazy mhegazy added Suggestion An idea for TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Sep 18, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Sep 18, 2017

There are a few complications for the proposal in the OP, first as you noted the compiler does not know that array.psuh is a mutating function and should not be allowed, (today we work around that by having ReadOnlyArray and ReadonlyMap); second, the mapped type creates a new type, and for a recursive type comparison can be expensive, since we are not using the compiler type identity checks, resulting in worse performance. We did contamplate adding it in the library when we added Readonly and Partial and then decided against that.

#10725 would seem a better solution here.

@Dean177
Copy link

Dean177 commented Feb 26, 2018

This will be possible in typescript 2.8 thanks to mapped types:

export type primitive = string | number | boolean | undefined | null
export type DeepReadonly<T> = T extends primitive ? T : DeepReadonlyObject<T>
export type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

declare const shallowReadOnly: Readonly<{ a: { b: number } }>
shallowReadOnly.a.b = 2 // Ok 😞

declare const readOnly: DeepReadonly<{ a: { b: number } }>
readOnly.a.b = 2 // Error 🎉

@esamattis
Copy link

Does it work for Arrays?

@Dean177
Copy link

Dean177 commented Mar 12, 2018

With a small modification it does:

export type primitive = string | number | boolean | undefined | null
export type DeepReadonly<T> =
  T extends primitive ? T :
  T extends Array<infer U> ? DeepReadonlyArray<U> :
  DeepReadonlyObject<T>

export interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

export type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

const foo: DeepReadonly<Array<number>> = [1, 2, 3]
foo[3] = 8 // Index signiture in type 'ReadonlyArray<number>' only permits reading

(ReadonlyArray is already a thing: https://www.typescriptlang.org/docs/handbook/interfaces.html#readonly-properties)

EDIT: Thanks @cspotcode & @mkulke

@cspotcode
Copy link

@Dean177: It doesn't make the elements of the array deeply readonly, correct? That seems like a big limitation. I tried to implement it myself and couldn't. I got errors about DeepReadonly circularly referencing itself. Seems like the numeric index signature causes problems.

@mkulke
Copy link
Contributor

mkulke commented Mar 16, 2018

@cspotcode would this work?

type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonly<T> =
  T extends any[] ? DeepReadonlyArray<T[number]> :
  T extends object ? DeepReadonlyObject<T> :
  T;

interface Step {
  length: number;
}

interface Trip {
  mode: 'TRANSIT' | 'CAR';
  steps: Step[];
}

type Trips = Trip[];

function mgns(trips: DeepReadonly<Trips>): void {
  const trip = trips[0];
  if (trip === undefined) {
    return;
  }
  trips.pop(); // readonly error
  trip.mode = 'WALK'; // readonly error
  trip.steps.push({ length: 1 }); // readonly error
  const step = trip.steps[0];
  if (step === undefined) {
    return;
  }
  step.length = 2; // readonly error
}

@cspotcode
Copy link

cspotcode commented Mar 17, 2018 via email

@RomkeVdMeulen
Copy link

RomkeVdMeulen commented Mar 29, 2018

Thank you all for your suggestions. I used them to come up with this:

export type DeepPartial<T> =
	T extends Array<infer U> ? DeepPartialArray<U> :
	T extends object ? DeepPartialObject<T> :
	T;

export type DeepPartialNoMethods<T> =
	T extends Array<infer U> ? DeepPartialArrayNoMethods<U> :
	T extends object ? DeepPartialObjectNoMethods<T> :
	T;

export interface DeepPartialArrayNoMethods<T> extends Array<DeepPartialNoMethods<T>> {}
export interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}

export type DeepPartialObject<T> = {
	[P in keyof T]?: DeepPartial<T[P]>;
};

export type NonFunctionPropertyNames<T> = {
	[P in keyof T]: T[P] extends Function ? never : P;
}[keyof T];

export type DeepPartialObjectNoMethods<T> = {
	[P in NonFunctionPropertyNames<T>]?: DeepPartialNoMethods<T[P]>;
};

I personally use it like this:

class MyType {
  constructor(init?: DeepPartialNoMethods<MyType>) {
    if (init) {
      Object.assign(this, init);
    }
  }
}

EDIT: oops, forgot to do array check before object check rather than after.

@g-harel
Copy link

g-harel commented Jun 12, 2018

This package's @types has been working well for me.

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/deep-freeze/index.d.ts

@nieltg
Copy link
Contributor

nieltg commented Jul 6, 2018

This is my implementation of DeepReadonly. I named it Immutable so it doesn't clash with Readonly.

type Primitive = undefined | null | boolean | string | number | Function

type Immutable<T> =
  T extends Primitive ? T :
    T extends Array<infer U> ? ReadonlyArray<U> :
      T extends Map<infer K, infer V> ? ReadonlyMap<K, V> : Readonly<T>

type DeepImmutable<T> =
  T extends Primitive ? T :
    T extends Array<infer U> ? DeepImmutableArray<U> :
      T extends Map<infer K, infer V> ? DeepImmutableMap<K, V> : DeepImmutableObject<T>

interface DeepImmutableArray<T> extends ReadonlyArray<DeepImmutable<T>> {}
interface DeepImmutableMap<K, V> extends ReadonlyMap<DeepImmutable<K>, DeepImmutable<V>> {}
type DeepImmutableObject<T> = {
  readonly [K in keyof T]: DeepImmutable<T[K]>
}

It handles ReadonlyArray and ReadonlyMap. It also handles Function types so their instances still can be called after being applied by this modifier.

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Aug 15, 2018
@RyanCavanaugh
Copy link
Member

Is there anything else needed from the type system side to adequately address the use cases here?

@simast
Copy link

simast commented Oct 28, 2018

Is there anything else needed from the type system side to adequately address the use cases here?

@RyanCavanaugh: There is no way to mark tuple types as readonly in the language right now.

@nickserv
Copy link
Contributor

I thought you could do that with tuple mapping in 3.1

@simast
Copy link

simast commented Oct 28, 2018

I thought you could do that with tuple mapping in 3.1

I don't believe this applies to actual tuple values, just mapped object types that have tuples as properties. Here is an example of a tuple in an object I am referring to:

const test: {
    readonly tuple: [number, string]
} = {
    tuple: [1, "dsffsd"]
}

test.tuple[0] = 2 // Works (but should be somehow marked as readonly)

@Offirmo
Copy link

Offirmo commented Nov 30, 2018

I'm accidentally mutating some constant data object. I put Readonly<> everywhere, but the compiler didn't catch anything, precisely because the bug is mutating deep in the object...

So that would be much needed!

@krzkaczor
Copy link

For those interested, DeepReadonly with all edge cases covered is part of ts-essentials package.

@icesmith
Copy link

icesmith commented Nov 22, 2019

Since TypeScript 3.7 is released, we can improve suggested implementations by using Recursive Type Aliases.

type ImmutablePrimitive = undefined | null | boolean | string | number | Function;

export type Immutable<T> =
  T extends ImmutablePrimitive ? T :
  T extends Array<infer U> ? ImmutableArray<U> :
  T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
  T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;

export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };

The suggested solutions works pretty well in most cases, but there are few problems because of replacing original types, like interface DeepImmutableArray<T> extends ReadonlyArray<DeepImmutable<T>> {}. For example, if you use immer and pass an old implementation of ImmutableArray to the produce() function, the draft will lack of array methods like push()

@bitjson
Copy link

bitjson commented Mar 4, 2020

Does anyone know how to create the type ImmutableUint8Array in this function?

type ImmutableUint8Array = unknown; // <- here
const firstByte = (bin: ImmutableUint8Array) => bin[0];

I'm trying to enable the new typescript-eslint/prefer-readonly-parameter-types, but Readonly<Uint8Array> fails because property buffer is not readonly, and none of the workarounds in this issue seem to handle this case.

Here are some "tests":

const canBeAssigned: ImmutableUint8Array = Uint8Array.of(0, 0);
const canBeSpread = [...canBeAssigned];
const canRecreateFromSpreadResult = Uint8Array.from(canBeSpread);
const functionRequiringType = (bin: ImmutableUint8Array) => bin;
const canAcceptNonMutableInstance = functionRequiringType(Uint8Array.of());

And it needs to pass the recursive isTypeReadonly.

Is it possible to specify a readonly TypedArray without a built-in DeepReadonly type?

@carpben
Copy link

carpben commented Jun 13, 2020

@bitjson , your comment doesn't relate directly to the topic of this issue - a native DeepReadonly generic type. I think comments here should refer to the need for such a feature, and/or how to implement it. I suggest you move your comment to StackOverFlow, or some other QA platform, where you are also more likely to get an answer.

@xenon
Copy link

xenon commented Jul 3, 2020

Here's my implementation of immutability. It's a combination of my own search to make an Immutable type along with this issue in which @icesmith informed us of the ReadonlyArray, ReadonlyMap and ReadonlySet types as I was only aware of doing T extends (infer U)[] for arrays and not considering maps or sets. Although unlike icesmith I didn't see a reason to split my Immutable type into separate sub-types. I've also made a function that will make a new declaration immutable.

export type Immutable<T> =
    T extends Function | boolean | number | string | null | undefined ? T :
    T extends Array<infer U> ? ReadonlyArray<Immutable<U>> :
    T extends Map<infer K, infer V> ? ReadonlyMap<Immutable<K>, Immutable<V>> :
    T extends Set<infer S> ? ReadonlySet<Immutable<S>> :
    {readonly [P in keyof T]: Immutable<T[P]>}

export function Immutable<T>(data: T): Immutable<T> {
    Object.freeze(data);
    if (data !== null) {
        for (let p in Object.getOwnPropertyNames(data)) {
            if (Object.prototype.hasOwnProperty.call(data, p) && typeof (data as any)[p] === 'object') {
                Immutable((data as any)[p]);
            }
        }
    }
    return data as Immutable<T>;
}

Example on an existing interface:

interface testobj {
    x: () => number;
    y: {n: number, f: () => string};
    a:  number[]
}
const o: Immutable<testobj> = ({
    x: () => { return 5; },
    y: {
        n: 5,
        f: () => 'hello',
    },
    a: [1, 2, 3],
});

o = o; // fails: 'o' is constant (because of const)
o.x() === 5; // true
o.y.n = 6; // fails: n 'readonly'
o.y.f = () => 'changed'; // fails: 'f' readonly 
o.y.f() === 'hello'; // true
o.a[2] = 4; // fails: index signature only permits reading

Make a type immutable without a proper type AND immutable at runtime:

const o = Immutable({
    ... (as o in the last example except untyped) ...
});
o = o; // fails: 'o' is constant (because of const)
o.x() === 5; // true, function call is allowed
o.y.n = 6; // fails: n 'readonly'
o.y.f = () => 'changed'; // fails: 'f' readonly 
o.y.f() === 'hello'; // true
o.a[2] = 4; // fails: index signature only permits reading

Of course if one wants both run-time readonly and typed readonly just combine them

const o : Immutable<testobj> = Immutable({ ...});

I didn't use export because I'm a typescript noob and I don't know how 'export' works. EDIT: It's actually pretty easy to use export and I've added it in.

@Offirmo
Copy link

Offirmo commented Oct 26, 2020

@icesmith unfortunately your suggestion breaks tuples... [FIX AT THE END of this post]

This code works (obviously):

function testTuple(tuple: [number, string]): void {}
// @ts-expect-error
testTuple([])
// @ts-expect-error
testTuple(['foo'])

This works:

function testTuple(tuple: readonly [number, string]): void {}
// @ts-expect-error
testTuple([])
// @ts-expect-error
testTuple(['foo'])

This DOESN'T work:

function testTuple(tuple: Immutable<[number, string]>): void {}
// @ts-expect-error
testTuple([])
// @ts-expect-error
testTuple(['foo'])

tuple type gets downgraded to Array[number | string]

I'm trying to find a solution...

[edit] it's trivial to solve it thanks to #26063 !
Just remove the second line and treat tuples as objects:

export type Immutable<T> =
  T extends ImmutablePrimitive ? T :
  //T extends Array<infer U> ? ImmutableArray<U> :   <-- not needed
  T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
  T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>; 

// This works for objects, arrays and tuples:
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };

@marekdedic
Copy link
Contributor

Hi,
I'd like to point out that the current approaches are incompatible with some very common types - for example, HTMLElement. Using the version from type-fest, I get error TS2345: Argument of type 'HTMLElement' is not assignable to parameter of type 'ReadonlyObjectDeep<HTMLElement>'.

As far as I understand this, the problem is that the type references itself somewhere down the line and that breaks this solution. Any TS-native version should probably handle this.

@Offirmo
Copy link

Offirmo commented Mar 23, 2022

@marekdedic how is my version working?

@marekdedic
Copy link
Contributor

@Offirmo not working - your version doesn't handle call signatures at all. See sindresorhus/type-fest#359 for relevant discussion and links. However, AFAIK, this cannot be done without a change in TS itself :/

@scottjmaddox
Copy link

@icesmith's / @Offirmo's solution works well enough within a single function, but the type checker is perfectly happy to pass an immutably typed value into a function that accepts a standard (mutably typed) value, which limits the usefulness. Rust is still the only language I'm aware of that gets this right.

@adrian-gierakowski
Copy link

Rust is still the only language I'm aware of that gets this right.

There’s also Haskell 😉

@alamothe
Copy link

alamothe commented Nov 2, 2022

Kotlin too.

@scottjmaddox
Copy link

There’s also Haskell 😉

Making everything immutable is cheating 😁

Kotlin too.

Haven't used to, but that's good to hear.

@Offirmo
Copy link

Offirmo commented Dec 9, 2022

@scottjmaddox could you share an example??

@scottjmaddox
Copy link

@Offirmo Something like this:

interface Foo { num: number }
function foo_mut(foo: Foo) { foo.num = 1; }
function foo_imm(foo: Immutable<Foo>) { foo_mut(foo); }

@FStefanni
Copy link

Hi,

just to add another corner case which is not covered: distinguish between a generic object type and a class type.
In all recursive type definitions, as DeepPartial, DeepReadonly, etc., we could like to skip classes, but recurse into plain objects.
For example:

class MyClass 
{
     f: number = 0;
};

type Struct = {
    a: number;
    b: {
        bb1: boolean;
        bb2: MyClass;
    };
};

In this example, I would like to perform a deep partial, skipping the field of type MyClass, obtaining something like:

type PartialStruct = {
    a?: number;
    b?: {
        bb1?: boolean;
        bb2?: MyClass;
    };
};

The rationale is: being a class instance value, allows for two possibilities:

  • it is present, with all its fields (how could you instantiate a partial of a class? new MyClass() will always create a full class instance!)
  • it is not present

I have performed a lot of tests, but none worked properly, so I believe that checking for a class type is truly a missing feature.

Finally, it could be nice to have a generic type utility to make something deep:

type Deepify = ...
type DeepPartial<T> = Deepify<T, Partial>; // this syntax does not work!

But also this seems not obvious (or currently not supported).

Regards.

@GermanJablo
Copy link

GermanJablo commented Jun 26, 2024

I have tried to compile all the contributions of this issue:

  • (comment) @Dean177 proposes the first implementation
  • (comment) When asked by @esamattis about whether it works with Arrays, @Dean177 realizes that it does not and corrects his implementation.
  • (comment) @RomkeVdMeulen makes a clear effort to exclude methods. I don't know if when he made that comment it was necessary, but at least now with the latest version of TS I can confirm that using readonly on objects does not allow you to use methods.
  • (comment) @nieltg modifies @Dean177's implementation to include maps and Function, and proposes calling the type DeepImmutable instead of DeepReadOnly to avoid collisions/confusion (I think it's a good idea)
  • (comment) @paps proposes a modification to "handle unknown". In my opinion, this comment can be dismissed, since the code doesn't actually do anything.
  • (comment) @carpben says that as of TS 3.4, object readonly should also work for arrays, but for some reason it doesn't, and it's still necessary to use ReadonlyArray
  • (comment) @carpben proposes checking if T extends object and leaving the primitives as fallback instead of the other way around, as it results in a cleaner and more compact syntax (I agree)
  • (comment) @icesmith has a comment with quite a few upvotes, but while he seems to be "correcting" the proposals so far, he basically says that (1) in TS 3.7 you can do type A = type B instead of interface A extends B {}, and (2) then mentions something about immer that has nothing to do with the discussion of this issue.
  • (comment) @xenon is the first to mention Sets. I don't know why no one mentioned it before as it was done with maps or arrays. It's a nice addition, as it allows reading methods like has to be used while prohibiting write methods like add.
  • (comment) @Offirmo notes that PR Improved mapped type support for arrays and tuples #26063 now improves Readonly so that it is no longer necessary to handle arrays separately. In fact, @Offirmo seems to suggest that not only is it not necessary, but it is preferable since it can cause problems. However, I don't understand his example very well, or at least I couldn't reproduce it in the playground. Anyway, I have verified that everything works fine without using ReadOnlyArray, (that is, mutation methods like push are prohibited but read methods like at or find are allowed).

Based on all this, my proposal is the following:

type DeepImmutable<T> =
  T extends Map<infer K, infer V>
    ? ReadonlyMap<DeepImmutable<K>, DeepImmutable<V>>
    : T extends Set<infer S>
      ? ReadonlySet<DeepImmutable<S>>
      : T extends object
        ? { readonly [K in keyof T]: DeepImmutable<T[K]> }
        : T;

Optional: if you like it, you can extract ImmutableMap, ImmutableSet and
ImmutableObject as separate types.

@Gr3q
Copy link

Gr3q commented Jun 28, 2024

@GermanJablo

What happens if I have this code? From usability perspective these should fail.

const immutableArr: DeepImmutable<T[]> = [];
const immutableObjArr: DeepImmutable<T>[] = [];
let mutableArr: T[] = []
mutableArr = immutableArr.filter(x => x);
mutableArr = immutableObjArr;

Edit:

T in this case should be

interface OnlyPrimitivesTestObj {
   test1: boolean;
   test2: string;
   test3: number;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests