`readonly` considered as a breaking change?
wiesson opened this issue ยท 35 comments
A readonly
has been introduced in the recent minor 1.2.9 release
a983b8b#diff-b68a2cbc8606fe37f9c2c72867a87ff50a11c2b38a6d9de45a94281f02bf0a4eR252
Is this considered to be a breaking change? The change makes sense to me and it looks useful, but it in my case, I haven't explicitly marked props as readonly
. My build breaks in ts strict mode and in order to fix it, I have to mark all props as readonly
(I haven thought much about it before, but to me props are always immutable) where I pass data from useSnapshot
.
Thanks for reporting.
Hm, I thought it's a proper fix in types, because it's frozen in JS.
But, I overlooked the breaking types in user code.
Let me add a note at least in the release page.
Let's see how this is troublesome for others.
If it causes more trouble than benefits, we can consider reverting it.
Let's see how this is troublesome for others.
Good idea! I'll stay on 1.2.8
and until I have added readonly to my props (or somebody else gives a hint how to fix it ๐
)
This also broke my build. I also agree that it is a useful change even though it is painful in the short term to change types across my whole code base. I do foresee this being troublesome for a lot of users. Would it make sense to add a small info section about this in the README?
Thanks for the feedback. I'm not sure if the README is the right place. I added a note in the release note.
I thought about some migration path, but I don't thin we have a way to warn it and pass the build. So, adding a small info makes sense. Maybe at the top of README?
Can you share some small code snippet that breaks with readonly
. Would love to learn the real cases.
I found that any case where the type of a key is an array of something, i.e. string[]
. With the new update, I'm forced to change this to readonly string[]
and make sure anything using or passing that value when read is typed as readonly string[]
instead of string[]
.
Yeah, sorry, I know how it's causing the error. My question out of curiosity is how/where you specify string[]
in your app code.
Let me know if this helps:
import { proxy } from 'valtio'
interface ProxyType {
someKey: readonly string[]
}
const testProxy = proxy({ someKey: [] }) as ProxyType
function doAThing (param: string[]): void {
console.log(param)
}
// The call below throws this:
// Argument of type 'readonly string[]' is not assignable to parameter of type 'string[]'.
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
doAThing(testProxy.someKey)
Thanks. ProxyType shouldn't be readonly.
I guess you use useSnapshot
(or snapshot
?), so it's more like this?
import { proxy, useSnapshot } from 'valtio'
interface ProxyType {
someKey: string[]
}
const testProxy = proxy<ProxyType>({ someKey: [] })
function doAThing (param: string[]): void {
console.log(param)
}
// in component
const snap = useSnapshot(testProxy)
doAThing(snap.someKey) // snap.someKey is `readonly string[]`
What about using a wrapper hook for gradual migration?
import { useSnapshot as useSnapshotOrig } from 'valtio'
type DeepWritable<T> = {
-readonly [P in keyof T]: DeepWritable<T[P]>
}
const useSnapshot = <T extends object>(proxyObject: T) => {
const snap = useSnapshotOrig(proxyObject)
return snap as DeepWritable<T>
}
https://codesandbox.io/s/github/mysterybear/valtio-play
@dai-shi should this be throwing a type error? I've wrapped the HouseT
prop type with the Readonly
utility type and still getting TS2322
Ah, someone on TS Discord helped me out
type HouseViewProps = {
house: DeepResolveType<HouseT>
}
Maybe we could use some docs on this DeepResolveType
utility type?
Thanks for reporting. I notice that this is a larger issue than I expected. It's not just about readonly
change. It reveals the potential design issue.
So, we didn't think DeepResolveType
as public API (not directly exported from index.ts). Somebody already reported about the difficulty of typing caused by DeepResolveType
(not related with readonly
.) #292
The solution was:
const { houses } = useSnapshot(store) as HouseT
For most users, DeepResolveType didn't make much sense because it was only useful when we use Suspense features. readonly
changed this...
I would like to fix this problem (both of them) somehow. It may require a help from TS experts. There's one thing I want to solve: https://twitter.com/dai_shi/status/1480523055105724419
@dai-shi someone on TS Discord says
I think the pattern he's looking for is impossible, trying to contain an explicit reference to a type without exporting it.
https://discord.com/channels/508357248330760243/885582487999365200/933899136850341979
That's good to know. Then, I will give up using const enum
. Let me create a new PR, which technically solves #292, but it doesn't help this issue.
I don't mind the readonly
change (although I agree this should've been a major release).
But I wonder if you should also export the Snapshot
type utility to recursively convert types to readonly.
Otherwise, I believe I have to create or import a separate DeepReadOnly
utility type.
Thanks for the feedback.
(although I agree this should've been a major release).
(I feel like I shouldn't have merged that PR, if I knew this.)
I have to create or import a separate DeepReadOnly utility type.
but, that's what I'm suggesting if you really need it. I really want to hide Snapshot
type as an implementation detail.
I stumbled upon a minor issue with readonly:
- I receive some data from api that I want to persist in valtio
- I then retrieve it via
useSnapshot
and need to pass to another api call - I now get type error due to added
readonly
Is there any way to not add them and keep original types?
Hi, my suggestion in your case is this (or that):
const snap = useSnapshot(state) as typeof state
Does it work? Maybe we should add this in readme.
Hi, my suggestion in your case is this (or that):
const snap = useSnapshot(state) as typeof stateDoes it work? Maybe we should add this in readme.
Does the trick, but feels a bit like a "hack around". Would it be viable to instead introduce something like useReadonlySnapshot
for those who want these strict typings and perhaps enforce it over useSnapshot
via valtios eslint plugin with added rule?
I think there will be a lot of use cases where user's don't need to mutate these values, yet they have to be passed on to an api or something else that doesn't expect that readonly addition.
I prefer to make it the other way around: useSnapshotLooselyTyped
.
I think there will be a lot of use cases where user's don't need to mutate these values, yet they have to be passed on to an api or something else that doesn't expect that readonly addition.
I don't disagree with this. It's something I really overlooked when reviewing the PR. But, there are opposite opinions from users. So, at least from the library perspective, it should use correct typing.
That makes sense, perhaps useProxy
can be re-used for this? To make naming nicer. This might also help with upgrading from older versions of valtio without that babel macro?
useProxy
macro is totally a different one after v1, so we shouldn't mix it.
currently I'm leaning to a recipe in readme rather than an exported function in utils, because there could be variations for various use cases.
Let's see what others think.
We have a project where we are using a generated API types where readonly
is not supported, which made the process even longer. But we finally have it done!!
I just wanted to share a Typescript snippet that we used to ease the process...
declare type AnyFunction = (...args: unknown[]) => unknown
export type DeepReadonly<T> = T extends (infer R)[]
? DeepReadonlyArray<R>
: T extends AnyFunction
? T
: T extends object
? DeepReadonlyObject<T>
: T
export type DeepReadonlyArray<T> = ReadonlyArray<DeepReadonly<T>>
export type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}
@dai-shi would it maybe be helpful to export a similar utility to ease the process of migration?
You don't need such a complicated type util.
I hope eventually TypeScript supports it: microsoft/TypeScript#13923
Overall, I think people wouldn't necessarily want to revert readonly
change. It's technically type breaking, so, to mitigate such cases, let's add a note in readme. Will open a PR.
Folks, I've got a better solution! With #359, we can do Module Augmentation.
https://codesandbox.io/s/react-typescript-forked-qphpi?file=/src/App.tsx:46-125
declare module "valtio" {
function useSnapshot<T extends object>(p: T): T;
}
fyi for others who found this thread, you can't add this module augmentation in decs.d.ts
โ it will override the entire module def โ can add it in any other regular ts file, tho
Stumbled on this as well earlier this week. I know the discussion has long been over but let me contribute anyway
Can you share some small code snippet that breaks with readonly. Would love to learn the real cases.
Here's one such case: Typescript Playground Link
In my case method consuming the value was defined in another module and it uses auto-generated type annotations of server responses from yet another module, which don't have readonly keywords anywhere in them.
I like the idea of having readonly properties on objects returned from useSnapshot
and have awaited promise values unwrapped. And the only thing that TS didn't like seems to be when we're attempting to pass ReadonlyArray
's in place of Array
's (same for readonly
tuples), so rather than re-defining it as
function useSnapshot<T extends object>(p: T): T;
as was suggested above I went for a different approach and just undid readonly flags on arrays. But in order to do this I had to rely on the "internal" export
// somewhere in my lib-env.d.ts
import 'valtio'; // probably unnecessary because of the next line
import { INTERNAL_Snapshot } from 'valtio';
type AnyFunction = (...args: any[]) => any;
type AnyArray = readonly unknown[];
type MutableArrays<T> = T extends AnyFunction ? T :
T extends AnyArray
? { -readonly [K in keyof T]: MutableArrays<T[K]> }
: { [K in keyof T]: MutableArrays<T[K]> }; // prettier-ignore
declare module 'valtio' {
function useSnapshot<T extends object>(p: T): MutableArrays<INTERNAL_Snapshot<T>>;
}
Personally I would have prefer that to be default behaviour. It defeats the purpose, but if this is something that everybody has to dance around, maybe it is worth making the default
Can you share some small code snippet that breaks with readonly. Would love to learn the real cases.
Here's one such case: Typescript Playground Link
To me, it looks like exactly the case we want to warn.
In my case method consuming the value was defined in another module and it uses auto-generated type annotations of server responses from yet another module, which don't have readonly keywords anywhere in them.
Yeah, that's problem, but it's only the user who know it. So, as
should be the right solution.
But in order to do this I had to rely on the "internal" export
In the next version, we don't unwrap promises, so the type should be simpler. We may revisit about considering exporting the snapshot type.
Personally I would have prefer that to be default behaviour. It defeats the purpose, but if this is something that everybody has to dance around, maybe it is worth making the default
fwiw, the change is requested from a user, and it's correct typing and allows to catch errors at compile time. At this point, there's no reason to go back.
To use the same deep-readonly method as the library itself (without re-implementing it as suggested above), I just did this and it seems to work as expected:
type ReadonlyCat = ReturnType<typeof useSnapshot<Cat>>;
I didn't know it was possible.
Seems like it's already supported in TS4.7.
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#instantiation-expressions
Thanks for sharing!
And this: https://tsplay.dev/WPJZZN
fyi for others who found this thread, you can't add this module augmentation in
decs.d.ts
โ it will override the entire module def โ can add it in any other regular ts file, tho
Thank you, this was causing me problems as well. I was putting it into global.d.ts
which also override the whole module. I put it in the index.ts file of app entry and it doesn't override, but augments.
import { proxy, useSnapshot } from 'valtio';
type DeepWritable<T> = {
-readonly [P in keyof T]: DeepWritable<T[P]>;
};
const initialState = Object.freeze({
message: undefined as string | undefined,
list: [] as
| Array<{
name: string;
children: { name: string }[];
}>
| undefined,
});
export type InitialState = DeepWritable<typeof initialState>;
const state = proxy(initialState);
export const updateStateModel = (updatedState: Partial<InitialState>) => {
Object.keys({ ...updatedState }).forEach(key => {
state[key] = updatedState[key];
});
};
export const useStateModel = () => useSnapshot<InitialState>(state) as InitialState;
const Demo = () => {
const { list } = useStateModel();
updateStateModel({ list }); // No more readonly trouble
};