Only allow returning known fields/properties when using set
(v4, TypeScript)
#1318
-
I'm not sure if I'm missing something or if my types are misleading, but I would like to only allow returning fields/properties from a Here is a real world example: import create from "zustand";
export type ErrorCode = "error1" | "error2"
interface ErrorsStoreState {
activeErrors: ErrorCode[]
activateError: (errorCode: ErrorCode) => void
deactivateError: (errorCode: ErrorCode) => void
}
/** Based on https://github.com/pmndrs/zustand/blob/main/docs/guides/typescript.md#basic-usage */
const useErrorsStore = create<ErrorsStoreState>()((set) => ({
activeErrors: [],
activateError: (errorCode) => {
set((state) => {
const activeErrors = state.activeErrors
// only add the error if it is not active yet
if (activeErrors.includes(errorCode)) {
return { activeErrors }
} else {
// 🚨🚨🚨 `nonsense` is the problem here. It does not exist on `ErrorsStoreState`
return { activeErrors: [...activeErrors, errorCode], nonsense: 'abc' }
}
})
},
deactivateError: (errorCode) => {
set((state) => ({
activeErrors: state.activeErrors.filter((code) => code !== errorCode),
}))
},
})) As you can see, one can freely add a property (here: Are my typings wrong or is this expected? I was just wondering if/why the Update: I know that TypeScript is not doing exact type checks out of the box, but this can be implemented with helper types. Was just wondering if this is implemented already for Zustand or if my usage of the
set((state) => {
const activeErrors: ErrorsStoreState['activeErrors'] = state.activeErrors
// only add the error if it is not active yet
if (!activeErrors.includes(errorCode)) {
activeErrors.push(errorCode)
}
return { activeErrors }
}) |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 4 replies
-
I don't know if it's possible in TypeScript. cc: @devanshj Your workaround is mutating the state, which is bad. |
Beta Was this translation helpful? Give feedback.
-
Your usage is correct, it's just that zustand doesn't and hasn't considered involving in hacks to get around TypeScript's structural types. It's true that exact types would be useful here (as you said to catch typos etc) but the issue is better handled on TypeScript's end (microsoft/TypeScript#12936) so that we get good errors etc. Nevertheless you can still implement them in a user-land middleware like so... https://tsplay.dev/WGRykm (Try the playground code locally, it doesn't compile in playground because of microsoft/TypeScript-Website#2552) import create, { StateCreator, StoreMutatorIdentifier } from "zustand"
type WithExactTypeCheck =
< T
, Mps extends [StoreMutatorIdentifier, unknown][] = []
, Mcs extends [StoreMutatorIdentifier, unknown][] = []
>
(f: StateCreator<T, [...Mps, ["withExactTypeCheck", never]], Mcs>) =>
StateCreator<T, Mps, [["withExactTypeCheck", never], ...Mcs]>
declare module "zustand" {
interface StoreMutators<S, A> {
withExactTypeCheck:
S extends { setState: infer F extends (...a: never[]) => unknown }
? & Omit<S, "setState">
& ( Parameters<Parameters<F>[0]>[0] extends infer T ?
Parameters<F> extends [unknown, ...infer R] ?
{ setState<A extends [Exact<A[0], T | Partial<T>>, ...R]>(...a: A): void
setState<A extends [(state: T) => Exact<ReturnType<A[0]>, T | Partial<T>>, ...R]>(...a: A): void
} : never : never
)
: never
}
}
type Exact<T, U> =
T extends U
? U extends unknown
? T extends (...a: infer Ta) => infer Tr
? U extends (...a: never[]) => infer Ur
? (...a: Ta) => Exact<Tr, Ur>
: never :
T extends object
? { [K in keyof T]:
K extends keyof U ? Exact<T[K], U[K]> :
never
} :
U
: never :
U
const withExactTypeCheck =
(f => f) as WithExactTypeCheck Here's a screenshot... You can also use a much more generic const typeCheckExact = (x => x) as
<T extends [Exact<T[0], U>], U>(...t: T) => U
type Exact<T, U> =
T extends U
? U extends unknown
? T extends (...a: infer Ta) => infer Tr
? U extends (...a: never[]) => infer Ur
? (...a: Ta) => Exact<Tr, Ur>
: never :
T extends object
? { [K in keyof T]:
K extends keyof U ? Exact<T[K], U[K]> :
never
} :
U
: never :
U Throw in let x: { a: { b: { c: number } } } = typeCheckExact({ a: { b: { c: 1, hello: "world" } } }) So in our case we can write Just some ideas, the code snippets here are not heavily tested and nor I would recommend you to use them if you're not comfortable. If I wanted to prevent typos and I don't work with TypeScript geniuses then I'd probably implement a much simpler const checkExcessProperty = (x => x) as
<T extends [CheckExcessProperty<T[0], U>]>
(...t: T) => U
type CheckExcessProperty<T, U> =
T extends U
? U extends unknown
? { [K in keyof T]:
K extends keyof U ? T[K] :
never
}
: never
: U Again I don't know what is the right way to go about it for you. As for zustand, it's probably a good idea to wait for microsoft/TypeScript#12936. Also if someone feels that TypeScript should do excess-property-checking in this scenario out-of-the-box (like it does for literals) then they can open an issue on the TypeScript repo (or find-and-upvote a duplicate one if any). |
Beta Was this translation helpful? Give feedback.
-
I filed microsoft/TypeScript#51010, we could consider adding shallow exact type checking in |
Beta Was this translation helpful? Give feedback.
-
Has anyone figured out how to get the I couldn't for the life of me get them to work together wrapped up into one singular zustand middleware. |
Beta Was this translation helpful? Give feedback.
Your usage is correct, it's just that zustand doesn't and hasn't considered involving in hacks to get around TypeScript's structural types. It's true that exact types would be useful here (as you said to catch typos etc) but the issue is better handled on TypeScript's end (microsoft/TypeScript#12936) so that we get good errors etc.
Nevertheless you can still implement them in …