-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # 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: