-
-
Notifications
You must be signed in to change notification settings - Fork 22
Struct.evolve forced type signature makes for an invalid transformation object #376
Comments
There's no error because type A = Partial<{
a: (a: number) => unknown
}>
declare const x: Record<string, string>
const a: A = x |
It is not, practically speaking, because it lets you pass non functions as values, which breaks it at runtime with no compile time error. Not sure about what’s the best solution would be, but surely it’s not, at least for the properties of the original struct, they should be forced to be typed as functions |
Well I'm just explaining why, with the current typings, it can't report an error. Practically speaking you are not supposed to force the types at the call site I guess, but maybe @jessekelly881 can cime in. |
the dirtiest solution I see is to check the second argument type with a |
I tried this approach and it seems to work. Haven't tested too many cases, feel free to roast it :) import * as Struct from "@effect/data/Struct"
type Transformed<O, T> = unknown & {
[K in keyof O]: K extends keyof T ? (T[K] extends (...a: any) => any ? ReturnType<T[K]> : O[K]) : O[K]
}
type PartialTransform<O, T> = {
[K in keyof T]: T[K] extends (a: O[K & keyof O]) => any ? T[K] : (a: O[K & keyof O]) => unknown
}
export const evolve: {
<O, T>(t: PartialTransform<O, T>): (obj: O) => Transformed<O, T>
<O, T>(obj: O, t: PartialTransform<O, T>): Transformed<O, T>
} = Struct.evolve
const mystruct = { a: 42, b: `hello` }
const correct = evolve(mystruct, { a: () => 42 }) // compile ✅
const goodCatch = evolve(mystruct, { a: `not a function` }) // error ✅
const solved = evolve<typeof mystruct, Record<string, string>>(mystruct, { a: `not a function` }) // error ✅
const stillPossible = evolve<typeof mystruct, Record<string, () => number>>(mystruct, { a: () => 42, b: () => 34 }) // compile ✅ |
I will try to test this when I'm back home in a few days(currently traveling without a computer) but this solution looks correct at a glance. I do agree that the type passed to the generic should be restricted if it can be but like @gcanti mentioned, I don't know why you would ever call it this way. :P |
As said before, I can't reproduce it now, I can only by fixing the type in the function call to a type you'd never manually write in such place. But I actually discovered it at runtime without an explicit annotation, so it actually happened once by itself. It crashed saying that Btw I was doing something by creating a partial transformation object with a dynamic key, so the type got widened a bit and resulted in no error. Again, I have no example to show, but it surely is reproducible. |
any news on this? |
"Again, I have no example to show, but it surely is reproducible." With a concrete example we may consider guarding against but as far as I can see |
The subtle problem is that I too had structures, then they somehow (I typed them wrongly in another place, changed some mapping functions in between or similar) got to a |
This is a good enough example I guess. import * as Record from "@effect/data/ReadonlyRecord"
import * as Struct from "@effect/data/Struct"
const mystruct = { a: 42, b: `hello` }
const bad = Struct.evolve(mystruct, Record.fromEntries([[`a`, `not a function`]])) // not an error ❌ This correctly fails at compile time with the suggested fix, but again, I'm not 100% sure about the fix itself, it may have flaws, just wanted to point out this could happen quite easily, even by mistake. |
version: 0.12.8
You can force an invalid type signature to
Struct.evolve
by explicitly annotating its generics during usage.In the example, the "good" row is correctly reporting an error, while the "bad" one isn't while it should.
This makes the second call to
Struct.evolve
fail at runtime cause it's expecting a function to call and instead if finds not a function.I can only reproduce it by forcing its generics, but it first happened to me without forcing them, in a complex scenario when I used a generic key to create a transformation object on the fly. The type for the values of the transformation object doesn't seem relevant, as long as it's (somehow) compatible with the to be evolved struct.
The text was updated successfully, but these errors were encountered: