#help with zod types and z.object
67 messages · Page 1 of 1 (latest)
Preview:```ts
...
function createObject<
One extends Schema,
Two extends Schema | undefined = undefined
(args: {
required: One
optional?: Two
}): z.ZodObject<
One & (Two extends undefined ? {} : Two)
...```
i'm having trouble figuring out why the types don't overlap and allow a single output type rather than overloads
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
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)
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
sorry, i don't know what that means. "write the overloads into a type assertion"?
here's your last playground with type assertions added:
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
}
...```
this is saying "trust me, TS, i know what i'm doing"
is it not enough to enforce a type at the function signature?
no, because of this
this is so confusing
what part is confusing?
why i'd need to have :InferOutput<One, Two> three times
here's a much simpler example of the same problem, which might be easier for us to gesture at:
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'.
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
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?
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"
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?
it doesn't really matter that it's a return type. here's another example without a function involved:
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'.
so it's just conditional types in general then?
yep
ok i follow so far
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
don't they get resolved into a union?
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)
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?
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
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
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)
ok, could you check back in 20 mins or so, i'm going to give that link a slow read through
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
it's no wonder i'm banging my head all day if this is the issue
trying to solve a literally impossible problem 😛
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
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)
i think when as is involved you need to extract into a separate variable which adds an extra step
that's annoying
ok, time to read through this link
@fathom shuttle i have a question
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
i've taken the example from this page and made a tspg
Create types which act like if statements in the type system.
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?
look at where the errors are. the problem is that the values you're returning from the implementation don't align with the return type (as far as TS can tell). in the declared function there are no return values to be checked
createLabel is simply returning T whereas createLabelFromObject needs to select a property from an object conditionally, which introduces more logic?
i don't think i understand what you're asking here—can you rephrase the question?
said another way, it's the same reason the x1 line has an error here and the x2 line does not. in the latter there's no value to check:
const x1: number = 'not a number'
// ^^
// Type 'string' is not assignable to type 'number'.
declare const x2: number
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