-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
The same for |
Having a 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") |
This has somewhat odd behaviour for callables, e.g. when calling
|
@mprobst I just ran into this issue using the same type... Any idea how to fix this? |
There are a few complications for the proposal in the OP, first as you noted the compiler does not know that #10725 would seem a better solution here. |
This will be possible in typescript 2.8 thanks to mapped types:
|
Does it work for Arrays? |
With a small modification it does:
( EDIT: Thanks @cspotcode & @mkulke |
@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 |
@cspotcode would this work?
|
Yeah, that mostly does the trick, thanks! But why are you stripping
methods off of the readonly objects? Is it because methods might trigger
mutations?
The only change I would make is replacing `T extends any[]` with `T extends
ReadonlyArray<any>`.
EDIT: @mkulke another thought: Function properties might also be objects with nested properties. Maybe it's better to map them as `DeepReadonlyObject`. This will preserve the nested properties and strip off the invocation signatures.
…On Fri, Mar 16, 2018 at 4:32 PM, Magnus Kulke ***@***.***> wrote:
@cspotcode <https://github.com/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
}
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#13923 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAW-uCsk2KXVLWzWM_i8s0vgq4uKOeGYks5tfCFagaJpZM4L5Rm3>
.
|
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. |
This package's https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/deep-freeze/index.d.ts |
This is my implementation of 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 |
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. |
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) |
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! |
For those interested, |
Does anyone know how to create the type type ImmutableUint8Array = unknown; // <- here
const firstByte = (bin: ImmutableUint8Array) => bin[0]; I'm trying to enable the new typescript-eslint/prefer-readonly-parameter-types, but 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 |
@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. |
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. |
@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 ! 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]> }; |
Hi, 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. |
@marekdedic how is my version working? |
@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 :/ |
@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. |
There’s also Haskell 😉 |
Kotlin too. |
Making everything immutable is cheating 😁
Haven't used to, but that's good to hear. |
@scottjmaddox could you share an example?? |
@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); } |
Hi, just to add another corner case which is not covered: distinguish between a generic object type and a class type. 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:
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. |
I have tried to compile all the contributions of this issue:
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 |
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:
|
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...):
The text was updated successfully, but these errors were encountered: