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 · 38 comments
Open

Suggestion: DeepReadonly<T> type #13923

mprobst opened this issue Feb 7, 2017 · 38 comments

Comments

@mprobst
Copy link
Contributor

@mprobst 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

This comment has been minimized.

Copy link

@felixfbecker felixfbecker commented Feb 7, 2017

The same for Partial

@iRath96

This comment has been minimized.

Copy link

@iRath96 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")
@mprobst

This comment has been minimized.

Copy link
Contributor Author

@mprobst 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

This comment has been minimized.

Copy link

@ChuckJonas ChuckJonas commented Sep 18, 2017

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

@mhegazy

This comment has been minimized.

Copy link

@mhegazy 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

This comment has been minimized.

Copy link

@Dean177 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

This comment has been minimized.

Copy link

@esamattis esamattis commented Mar 12, 2018

Does it work for Arrays?

@Dean177

This comment has been minimized.

Copy link

@Dean177 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

This comment has been minimized.

Copy link

@cspotcode cspotcode commented Mar 16, 2018

@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

This comment has been minimized.

Copy link
Contributor

@mkulke 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

This comment has been minimized.

Copy link

@cspotcode cspotcode commented Mar 17, 2018

@RomkeVdMeulen

This comment has been minimized.

Copy link

@RomkeVdMeulen 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

This comment has been minimized.

Copy link

@g-harel g-harel commented Jun 12, 2018

@nieltg

This comment has been minimized.

Copy link
Contributor

@nieltg 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

This comment has been minimized.

Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Aug 15, 2018

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

@simast

This comment has been minimized.

Copy link

@simast 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.

@nickmccurdy

This comment has been minimized.

Copy link

@nickmccurdy nickmccurdy commented Oct 28, 2018

I thought you could do that with tuple mapping in 3.1

@simast

This comment has been minimized.

Copy link

@simast 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

This comment has been minimized.

Copy link

@Offirmo 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

This comment has been minimized.

Copy link

@krzkaczor krzkaczor commented Dec 15, 2018

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

@masterkidan

This comment has been minimized.

Copy link

@masterkidan masterkidan commented Jan 24, 2019

@esamattis

This comment has been minimized.

Copy link

@esamattis esamattis commented Jan 25, 2019

I think the as const PR covers this too? #29510

@masterkidan

This comment has been minimized.

Copy link

@masterkidan masterkidan commented Jan 26, 2019

@epeli : Not quite... The DeepReadonly type can also be applied to classes as well...
Lets say we have a class like so

class Foo {
  public bar: number

  public isBar(): boolean {
     return bar == 42
  }
}

type ReadonlyFoo = DeepReadonly<Foo>;

class FooFactory {
    static FooBar () : ReadonlyFoo { 
       const a = new Foo();
      a.bar = 42;
      return a as ReadonlyFoo;
    }

The above will end up creating a readonly of the Foo class, I think as const can only be applied to objects, literals etc ... not to prototypes like the one described above.

@carpben

This comment has been minimized.

Copy link

@carpben carpben commented Feb 3, 2019

Is @epeli correct? Does #29510 effectively cover this?
@masterkidan, I guess you mean

   a.bar = 42 

But it seems you should be able to do something like this:

   return a as const

a is not a class but an object. Why do you assume you can't cast it as such?

@esamattis

This comment has been minimized.

Copy link

@esamattis esamattis commented Feb 3, 2019

Actually no I'm not :) I misunderstood it.

The as const is for literal types (object literals etc.) only and cannot be used for converting existing types.

@masterkidan

This comment has been minimized.

Copy link

@masterkidan masterkidan commented Feb 7, 2019

@carpben : Oops, thanks for catching that, I updated my earlier comment.

My point is , the type 'a' that is returned will no longer have the isBar() method.... as the as const is only for objects. Whereas with Readonly, we can expose the functions out as well.

The idea here is to expose out the properties of an object as readonly but also have some controlled methods that will update those properties/encapsulate some business logic needed for updating those properties.

@pauldraper

This comment has been minimized.

Copy link

@pauldraper pauldraper commented Mar 2, 2019

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,

Yes, it seems that this is getting close to C++ const (which IMO is one of the very best features of C++ and surprisingly uncommon elsewhere), where array.push is known to be a mutating function because it is not marked const in its declaration.

@paps

This comment has been minimized.

Copy link

@paps paps commented Apr 8, 2019

@nieltg I updated yours to handle unknown

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

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

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]>
}
@carpben

This comment has been minimized.

Copy link

@carpben carpben commented Apr 8, 2019

@paps @nieltg , my experience using a similar version of DeepReadonly<T> is that it doesn't handle well ReadonlyArray<T>, (and therefor, any type which originally has a sub array, and has been mapped by DeepReadonly<T>).

In Typescript an Array extends a ReadonlyArray, but not the other way around (as a ReadonlyArray type doesn't have certain methods such as push). I recommend changing
T extends Array<infer U> ? to T extends ReadonlyArray<infer U>?

@vidal7

This comment has been minimized.

Copy link

@vidal7 vidal7 commented Apr 8, 2019

@paps @nieltg , my experience using a similar version of DeepReadonly<T> is that it doesn't handle well ReadonlyArray<T>, (and therefor, any type which originally has a sub array, and has been mapped by DeepReadonly<T>).

In Typescript an Array extends a ReadonlyArray, but not the other way around (as a ReadonlyArray type doesn't have certain methods such as push). I recommend changing
T extends Array<infer U> ? to T extends ReadonlyArray<infer U>?

I think that TypeScript 3.4 is now correctly mapping readonly Array to ReadonlyArray. See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html

@carpben

This comment has been minimized.

Copy link

@carpben carpben commented Apr 9, 2019

Reading the release notes of Typescript 3.4, the native Readonly

type Readonly<T> = {
    readonly [K in keyof T]: T[K]
}

can now handle array like objects.
I guess it also means that the generic

type DeepImmutableObject<T> = {
	readonly [K in keyof T]: DeepImmutable<T[K]>
}

can now handle Array like objects.

Therefore, the DeepReadonly<T> can now look like this:

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

export type DeepReadonly<T> =
	T extends Primitive ? T :
		T extends Map<infer K, infer V> ? DeepReadonlyMap<K, V> :
			T extends object ? DeepReadonlyObject<T> : unknown

interface DeepReadonlyMap<K, V> extends ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> {}
type DeepReadonlyObject<T> = {
	readonly [K in keyof T]: DeepReadonly<T[K]>
}

But checking this in StackBlitz with Typescript 3.4, it seems the editor doesn't handle this gracefully for arrays at the moment. https://stackblitz.com/edit/typescript-2gxlgg

@vidal7 @paps @nieltg

@carpben

This comment has been minimized.

Copy link

@carpben carpben commented May 1, 2019

My DeepReadonly implementation:

export type DR<T> =
        T extends Function ? T : 
	T extends ReadonlyArray<infer R> ? IDRArray<R> :
	T extends Map<infer K, infer V> ? IDRMap<K, V> : 
	T extends object ? DRObject<T> :
	T

interface IDRArray<T> extends ReadonlyArray<DR<T>> {}

interface IDRMap<K, V> extends ReadonlyMap<DR<K>, DR<V>> {}

type DRObject<T> = {
	readonly [P in keyof T]: DR<T[P]>;
}

@paps @nieltg, Your implemenation checks first if the type parameter is a primitive (I have seen this pattern elsewhere as well). What advantage does it provide over this shorter implementation?

@jpike88

This comment has been minimized.

Copy link

@jpike88 jpike88 commented May 14, 2019

@carpben your implementation doesn't cover the delete command:

e.g. delete this.Preferences[key]

@carpben

This comment has been minimized.

Copy link

@carpben carpben commented May 14, 2019

@jpike88 This is not my experience. Here is a screenshot from VSCode on my device:
image

@jpike88 , I noticed you asked about this issue at sindresorhus/type-fest#34 . Have you come across an actual case where this implementation failed to cover the delete command? Can you provide a demo?

@hiyelbaz

This comment has been minimized.

Copy link

@hiyelbaz hiyelbaz commented May 16, 2019

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 🎉

I think this covers everything we need as of Typescript 3.4.

@jpike88

This comment has been minimized.

Copy link

@jpike88 jpike88 commented May 20, 2019

@carpben I can't reproduce it, might have been a brain fart, disregard

karlhorky added a commit to karlhorky/typescript-tricks that referenced this issue Jun 30, 2019
@andyfleming

This comment has been minimized.

Copy link

@andyfleming andyfleming commented Oct 8, 2019

With how complex these DeepReadOnly definitions are, it would be nice to see it provided directly by TypeScript.

@icesmith

This comment has been minimized.

Copy link

@icesmith 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()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.