Repository navigation
Nested conditional generic types yields boolean not assignable to true | false | undefined. #22630
Description
Activity
I've gotten a similar error but with an object instead.
interface Stamp { id: number imageUrl: string /** Hide from the list of new stamps. */ hidden: boolean } type StampOptions = Pick<Stamp, 'hidden'> const defaultOptions: StampOptions = { hidden: false } // The | StampOptions is something similar to React.Component#setState from @types/react declare function makeStamp<Keys extends keyof StampOptions> (id: number, options?: Pick<StampOptions, Keys>|StampOptions): Stamp function makeStampSeries<Keys extends keyof StampOptions> (seriesId: number, options?: Pick<StampOptions, Keys>|StampOptions, count = 10): ReadonlyArray<Stamp> { return Array.from({ length: count }, (_, i) => makeStamp(seriesId * 100 + i + 1, options)) }
Argument of type 'Pick<Stamp, "hidden"> | Pick<Pick<Stamp, "hidden">, Keys> | undefined' is not assignable to parameter of type 'Pick<Stamp, "hidden"> | Pick<Pick<Stamp, "hidden">, "hidden"> | undefined'. Type 'Pick<Pick<Stamp, "hidden">, Keys>' is not assignable to type 'Pick<Stamp, "hidden"> | Pick<Pick<Stamp, "hidden">, "hidden"> | undefined'. Type 'Pick<Pick<Stamp, "hidden">, Keys>' is not assignable to type 'Pick<Pick<Stamp, "hidden">, "hidden">'. Property 'hidden' is missing in type 'Pick<Pick<Stamp, "hidden">, Keys>'.This
Picktrickery is used to get, effectively, the same effect asPartial, but without allowing randomundefinedvalues.- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Mar 16, 2018 Griffork I'm not seeing any errors with your example.
Anders Hejlsberg (@ahejlsberg) My apologies, in my rush I missed one important part of the code, here's the updated sample:
type RecursivePartial<T> = { [param in keyof T]?: T[param] extends object ? RecursivePartial<T[param]> : T[param] }; interface UpdateValue<T> { $set: T extends object? RecursivePartial<T>: T; } interface ArrayUpdate<T> { $wildcard: WriteQuery<T>; } export type TypeWriteQuery<T> = T extends Array<infer U> ? Partial<ArrayUpdate<U>> : T extends object ? WriteQuery<T> : Partial<UpdateValue<T>> ; export type WriteQuery<T> = { [param in keyof T]?: TypeWriteQuery<T[param]> }; export async function Send<T>(Url: string, write?: WriteQuery<T>, options?: any): Promise<T[]> { return []; } ///----- Erroring code: interface Approved { approved: boolean; } Send<Approved>("doot", { approved: { $set: <boolean>true } });
It needed the object literal to be passed into the function call for it to work.
Edit: Casting the object literal directly to the type expected by the function
<WriteQuery<Approved>>works, so I'll use that for now.Best I can tell this is working as intended. The issue here is that the conditional type in
TypeWriteQuery<T>is distributive (see definition in #21316). Thus, whenTis a union type (such asboolean), the conditional type is applied to each of the constituents of the union type (i.e.trueandfalse). There is more discussion on this topic in #22596.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bugand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Mar 19, 2018 It's intended that it errors when the type is enforced by a function but not when its cast to or assigned to that type?
Argh, can't edit post on mobile.
It only worked above because of the cast.And it's
working as intendedthat any other type works fine, but booleans can't be passed to functions without a cast?RyanCavanaugh commented
on Mar 20, 2018 MemberMore actionsGriffork the linked issue demonstrates how to cause a conditional type to not distribute over unions
Ryan Cavanaugh (@RyanCavanaugh) I've read through the issue twice now and don't understand how anything in there solves my problem.
What I have read is this:
The fact that the unit type boolean has a finite number of values shouldn't lead to magically different behavior compared to its infinitely-domained counterparts. This is especially confusing because the user never wrote a union type in the above example.
Which seems to imply my above example is a bug, since every atomic type but boolean works this way.
Also I don't understand why boolean has to expand to
true|false. If I wantedtrue|falsethen I'd writetrue|false.Griffork The
booleantype is exactly equal totrue | false. There is zero difference between the two.The issue you're seeing isn't specific to
boolean. Any union of literal types will have the same issues. For example, using atype ApprovalStatus = "yes" | "no"instead ofbooleanalso wouldn't work in your example. In fact, your code only works withstringandnumberbecause they happen to be infinite types that (by necessity) are treated differently than union types.As I mentioned above, you can use the techniques in #22596 to control whether and how the
TypeWriteQuery<T>distributes over union types.Anders Hejlsberg (@ahejlsberg) apparently I'm missing something obvious, but when reading through #22596 I didn't see any "techniques" that solve my problem. Is it too much for me to ask for you to provide an example?
I suppose I understand what you're saying that boolean should be equal to a union of literal types, but that's not what I expect to happen when I write the word boolean (as opposed to true|false). When I write boolean I expect it to behave equally to the other built-in atomic types, since it is a built-in atomic type (the fact that it happens to have only two possible values is irrelevant I feel).
The other thing that feels inconsistent is that (at least in strict mode) boolean behaves differently to everything else because it's the only keyword that implies a union type.
Edit: fixed typos.
Griffork I'd change
TypeWriteQuery<T>to something like:type TypeWriteQueryDistributed<T> = T extends Array<infer U> ? Partial<ArrayUpdate<U>> : T extends object ? WriteQuery<T> : T extends null | undefined ? T : never; type Primitives<T> = T extends object | null | undefined ? never : T; export type TypeWriteQuery<T> = TypeWriteQueryDistributed<T> | (Primitives<T> extends never ? never : Partial<UpdateValue<Primitives<T>>>);
Anders Hejlsberg (@ahejlsberg) ok that seems to work (I have another error which I suspect is unrelated).
How does that work though? I don't understand why wrapping primitives in another layer of genericity helps.
Ok, the other error was indeed unrelated.
The other thing I've just noticed, which is what you were trying to tell me all along, is that fixing the boolean problem will also fix problems I would have had with trying to wrap an object that defines string or number literals, or any other union as value types, because we want them all to be treated as a group.
Still don't understand why this works though.
The
TypeWriteQueryDistributed<T>andPrimitives<T>types are both distributive: WhenTis a union type they are evaluated individually for each constituent in the union and then the results are unioned back together.Say you're resolving
TypeWriteQuery<string[] | string | boolean>.TypeWriteQueryDistributed<string[] | string | boolean>is the same asTWQD<string[]> | TWQD<string> | TWQD<true> | TWQD<false>(because the conditional type inTWQDis distributive). That in turn becomesPartial<ArrayUpdate<string>> | never | never | neverwhich is justPartial<ArrayUpdate<string>>.Similarly
Primitives<string[] | string | boolean>is the same asP<string[]> | P<string> | P<true> | P<false>. That in turn becomesnever | string | true | false, which is juststring | boolean. This type doesn't extendnever(i.e. it isn't an empty union).So, you end up with
Partial<ArrayUpdate<string>> | Partial<UpdateValue<string | boolean>>. Which presumably is what you were looking for. But I'm not really sure what your scenario is.My scenario is that I can have any JSON object stored in a database, and the database has very a specific query structure. Because we don't have typing information at runtime I'm trying to get compile-time checks for query objects.
This means that the following object:interface MyFoo { a: string; b: boolean; c: { d: boolean; e: string; x: string[]; y: boolean[]; z: { i: boolean; j: string; }[]; }; f: <boolean[]; g: string[] h: { i: boolean; j: string; }[]; }
Needs to become:
interface MyFooQuery { a?: UpdateValue<string>; b?: UpdateValue<boolean>; c?: UpdateValue<{ d: boolean; e: string; x: string[]; y: boolean[]; z: { i: boolean; j: string; }[]; }> | { //And so-on recursively d?: UpdateValue<boolean>; e?: UpdateValue<string>; x?: ArrayUpdate<string>; y?: ArrayUpdate<boolean>; z?: ArrayUpdate<{ i: boolean; j: string; }>; }; f?: ArrayUpdate<boolean>; g?: ArrayUpdate<string>; h?: ArrayUpdate<{ i: boolean; j: string; }>; }
Naturally, I don't want to have to manually type every single interface, since there's a lot of them.
Potential problem area:
Assuming the definition of ArrayUpdate is actually this:interface ArrayUpdate<T> { $set: T[]; }
The thing is, that
ArrayUpdate<string | boolean>is not the same asArrayUpdate<string> | ArrayUpdate<boolean>because the first one allows me to do this:var test: ArrayUpdate<string | number> = { $set: ["toot", 3] }
And the other one only permits this:
var test2: ArrayUpdate<string>| ArrayUpdate<number> = { $set: ["toot", "anothertoot"] }; //or var test3: ArrayUpdate<string>| ArrayUpdate<number> = { $set: [2, 3] };
but this errors:
var test4: ArrayUpdate<string>| ArrayUpdate<number> = { $set: ["toot", 3] };
Anders Hejlsberg (@ahejlsberg) does that clear my use-case up a bit or would you like me to provide more information?
No, I was just saying that I couldn't be sure exactly what your intent was. Hopefully the explanation I provided above gives you a better understanding of how distributive conditional types work so you can find the solution that best works for you.
Anders Hejlsberg (@ahejlsberg) you said:
So, you end up with
Partial<ArrayUpdate<string>> | Partial<UpdateValue<string | boolean>>. Which presumably is what you were looking for. But I'm not really sure what your scenario is.But as explained above what I want is
Partial<ArrayUpdate<string | boolean>> | Partial<UpdateValue<string | boolean>>. Is there a way to stop a type from being distributive?Edit: looking at the linked issue it looks like you're thinking of proposing a new syntax for this. I'm asking if there's any way I can rewrite my types to do it now, since I'd ideally like to do this at work within the month (and I can't use nightlys on a live work project).
typescript-bot commented
on Apr 6, 2018 ContributorMore actionsAutomatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
- locked and limited conversation to collaborators
on Jul 25, 2018
TypeScript Version: 2.8RC
Search Terms:
Code
End of friday, so I don't have enough time to make a proper repro, hopefully this will be enough to go by:
Expected behavior:
Works fine (unless I've made another mistake).
Actual behavior:
Errors that
booleanisn't assignable totrue | false | undefinedbecausebooleanisn't assignable tofalse.