#help with zod types and z.object

67 messages · Page 1 of 1 (latest)

wooden lion
#

idk how merge works exactly, assuming it's an intersection...

steep muralBOT
#
gerrit0#0

Preview:```ts
...
function createObject<
One extends Schema,
Two extends Schema | undefined = undefined

(args: {
required: One
optional?: Two
}): z.ZodObject<
One & (Two extends undefined ? {} : Two)

...```

inner kraken
#

i'm having trouble figuring out why the types don't overlap and allow a single output type rather than overloads

fathom shuttle
#

when you have a generic conditional return type like this you'll almost always need a type assertion. TS doesn't have the ability to analyze the control flow within the function to check whether it aligns with the conditional type. see https://github.com/microsoft/TypeScript/issues/33912, for example

GitHub

TypeScript is a superset of JavaScript that compiles to clean JavaScript output. - Issues · microsoft/TypeScript

#

people will often use overload signatures instead of this pattern to avoid the need for type assertions, though TS still doesn't typecheck the implementation against all of the overload signatures (so you don't get a static guarantee that the dependency relationships are correct)

inner kraken
#

yeah but afaik i'm working on a type assertion but just unable to get it working?

#

ideally i'd like to know how to write the overloads into a type assertion

#

i'm missing something, i just don't know enough to know what

fathom shuttle
#

sorry, i don't know what that means. "write the overloads into a type assertion"?

#

here's your last playground with type assertions added:

steep muralBOT
#
mkantor#0

Preview:```ts
...
function createObject<
One extends z.ZodRawShape,
Two extends z.ZodRawShape | undefined = undefined

(args: {
required: One
optional?: Two
}): InferOutput<One, Two> {
if (args.optional) {
const output = z
.object(args.required)
.merge(z.object(args.optional))
return output as InferOutput<One, Two> // <----- type assertion
}
const output = z.object(args.required)
return output as InferOutput<One, Two> // <----- type assertion
}
...```

fathom shuttle
#

this is saying "trust me, TS, i know what i'm doing"

inner kraken
#

is it not enough to enforce a type at the function signature?

inner kraken
#

this is so confusing

fathom shuttle
#

what part is confusing?

inner kraken
#

why i'd need to have :InferOutput<One, Two> three times

fathom shuttle
#

here's a much simpler example of the same problem, which might be easier for us to gesture at:

steep muralBOT
#
const f = <T extends boolean>(x: T): T extends true ? number : string => x ? 42 : 'hello world'
//                                                                       ^^^^^^^^^^^^^^^^^^^^^^
// Type 'string | number' is not assignable to type 'T extends true ? number : string'.
//   Type 'number' is not assignable to type 'T extends true ? number : string'.
fathom shuttle
#

TS doesn't have the smarts to reverse-engineer the control flow within the function's body in order to figure out whether it matches the conditional type

inner kraken
#

i appreciate the simpler example right now, my brain is fried

#

so i'm up to date on the jargon, is the return type on the function signature called anything special in TS?

fathom shuttle
#

as you can see the type of the expression x ? 42 : 'hello world' is just string | number, even though its contextually-typed as that conditional type

#

it's just a "return type". A extends B ? C : D is a "conditional type", so this is "a conditional return type"

inner kraken
#

f(): T versus f(): T extends true ? number: string
both are explicit return types, but latter is conditional, and that is where the problem is so far?

fathom shuttle
#

it doesn't really matter that it's a return type. here's another example without a function involved:

steep muralBOT
#
const x = Math.random() > 0.5
const y: typeof x extends true ? number : string = x ? 42 : 'hello world'
//    ^
// Type 'string | number' is not assignable to type 'string'.
//   Type 'number' is not assignable to type 'string'.
inner kraken
#

so it's just conditional types in general then?

fathom shuttle
#

yep

inner kraken
#

ok i follow so far

fathom shuttle
#

conditional types that can't be fully resolved to a single static type, anyway

#

in both of these examples it depends on some value that is only known at runtime

inner kraken
#

don't they get resolved into a union?

fathom shuttle
#

i think that's the important factor

#

no

#

conditional types are kinda more than a union. if you're familiar with boolean logic terms then a union is "or" but a conditional type is implication (if/then)

inner kraken
#

as an example,

function foo(args) {
  if (args.a) return args.a
  return args.b
}

this returns a union of string | boolean
does this hold any weight to the current issue?

fathom shuttle
#

the fact that string | boolean is not assignable to typeof args['a'] extends true ? string : boolean is the problem, yeah

#

it's the difference between "i'm going to the cinema or going to the park" (or/union type) vs "if it's raining i'm going to the cinema, otherwise i'm going to the park" (implication/conditional type)

#

the latter carries more information

inner kraken
#

so my function is compile time and can only ever be a union, and the conditional type wants it to be implication and this is the problem? this is how i've understood it so far

fathom shuttle
#

i guess? i personally just think of it as "TS doesn't analyze things this deeply" and leave it there

#

the issue i linked earlier has a bit of elaboration on the implementation details of the compiler

#

(Ryan who filed that issue is a TS maintainer, so he knows what he's talking about)

inner kraken
#

ok, could you check back in 20 mins or so, i'm going to give that link a slow read through

fathom shuttle
#

sure, feel free to @mention me if i don't respond right away

#

FYI i'm at the airport right now but have a bit over an hour until i board my flight

inner kraken
#

it's no wonder i'm banging my head all day if this is the issue

fathom shuttle
#

trying to solve a literally impossible problem 😛

inner kraken
#

ah ok then i'll mention you and won't expect an immediate reply, don't worry, you've put me on the right path and that's super helpful already

#

do you happen to know if theoretically type assertion would work no problem in TSDoc?

#

i guess via

/** @type InferOutput<One, Two> */
const output = z.object(args.required)
return output
fathom shuttle
#

i don't think TSDoc has extra superpowers here (or really anywhere? i know there are some differences but overall i think of TSDoc as equivalent to the normal TS type system)

inner kraken
#

i think when as is involved you need to extract into a separate variable which adds an extra step

fathom shuttle
#

that's annoying

inner kraken
#

ok, time to read through this link

inner kraken
#

the error only shows up if you implement rather than declare, and then don't have casting?

#

is the difference between createLabel and createLabelFromObject also an important factor to consider? createLabel is simply returning T whereas createLabelFromObject needs to select a property from an object conditionally, which introduces more logic?

fathom shuttle
fathom shuttle
fathom shuttle
steep muralBOT
#
const x1: number = 'not a number'
//    ^^
// Type 'string' is not assignable to type 'number'.
declare const x2: number
inner kraken
#
function createLabel<T extends number | string>(i: T): NameOrId<T> {
    return i
//  ^^^^^^
//  Type 'T' is not assignable to type 'NameOrId<T>'.
//    Type 'string | number' is not assignable to type 'NameOrId<T>'.
//      Type 'string' is not assignable to type 'NameOrId<T>'.
}
#

i see the difference now. so this is is erroring because by adding values to the function, TS has something to cross check with the return type

inner kraken
#

ok i think this thread is done

#

thank you @wooden lion