Repository navigation
Can't exclude undefined from indexed Partial with simple guard #31908
Description
Activity
jack-williams commented
on Jun 16, 2019 CollaboratorMore actionsDuplicate of #11483.EDIT: Nope, sorry. Misread the example. I'm guessing that maybe the narrowing code doesn't do the same level of probing to the expression type compared to when types are related. Is this a perf thing or just a missing feature?
That's right, it's not that TypeScript doesn't narrow the type of the expression
partial[key], it doesn't narrow the type ofvalue(which= partial[key]) either.RyanCavanaugh commented
on Jun 26, 2019 MemberMore actionsTypeScript doesn't know that
partial[key]is equivalent toT[K] | undefined, only that it's assignable to that type. Knowing that it's equivalent basically requires higher-order understanding of whatPartial<T>does by working backwards through the mapped type and reasoning about what kind of values come out the other side. If we had #22348 (which is a scary bag of worms), this would probably end up working out OK.Anyway with all that said, having the only inference position for
Tbe aPartial<T>is going to be a serious damper on your inference. It'd be better to write the signature astype NotUndefined<T> = T extends undefined ? never: T; function getValue<T, K extends keyof T>(partial: T, key: K): NotUndefined<T[K]> | null { const value = partial[key] if (value !== undefined) { return value as NotUndefined<T[K]>; } return null }
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on Jun 26, 2019 I might have a similar issue:
type MyEvents = 'start' | 'stop' | 'suspend'; type EventCounts = { [key in MyEvents]?: number}; const eventCounts: EventCounts = { start: 0 }; function foo<T extends MyEvents>(event: T) { // not working - undefined is not assignable to number const count: number = eventCounts[event] || 0; // working const foo: number | undefined = eventCounts[event]; const count2: number = foo || 0; // other workarounds const count3: number = (eventCounts[event] || 0)!; const count4: number = eventCounts[event] as number | undefined || 0; }
I didn't really understand the reasoning about
requires higher-order understanding of what Partial does
In my case this is more direct (optional index signature). Does this hit the same design limitation?
(I know that in this example the generic is only constructed, i could just use "event: MyEvents" non generic, that's also working, but not the point :)).
Reacted by Daniel PfefferkornIn the following example, it seems like the compiler does know that
Dict<K>[K]is equivalent tonumber | undefined, not just assignable, because the compiler itself generates the typenumber | undefinedforvalue, just not in time to narrow it:type Dict<K extends string> = { [key in K]?: number } function getValue<K extends string>(dict: Dict<K>, key: K): number { const value = dict[key] // value: Dict<K>[K] if (value) { // or: if (value !== undefined) // Error: Type 'number | undefined' is not assignable to type 'number'. return value } throw new Error() }
Ryan Cavanaugh (@RyanCavanaugh) Thanks for the input. PR #22348 does seem related, and in particular issue #22137 that it aims to fix. The function in my example is artificial, like the function in this example; it's a reduced case, not necessarily a good helper function I would write.
So my understanding is there is one solution, which is scary, which is to make type guards basically introduce an
Exclude/Extract(though I could have sworn this happens in some cases).What about making it so that indexing a mapped type checks if you are indexing with the key type, or something assignable to the key type, something like that? Something like this must be happening for non-generics, because indexing a
Dict<string>by string works. In other words, would it be incorrect to eagerly simplifyDict<K>[K]tonumber | undefined?David Greenspan (@dgreensp)
I have encountered the same issue. Given the example you provided:function getValue<T, K extends keyof T>(partial: Partial<T>, key: K): T[K] | null { const value = partial[key] if (value !== undefined) { return value // error: TypeScript thinks this can be undefined... } return null }
Basically the type of
valueisPartial<T>[K]. Note, it is notPartial<T>[K] | undefined. So narrowing down, removingundefined, does not change anything on the type. And since structure ofPartial<T>is not known yet, the type can at this point only be described in the generic wayPartial<T>[K].Ryan Cavanaugh (@RyanCavanaugh)
Given this:type Partial<T> = { [P in keyof T]?: T[P]; }
What if focus is not on general type narrowing, but only related to
indexed typesand?-indices, which should be equivalent forany indexed type(correct me if I am wrong). So you can narrow downundefined !== t[p]based onindexed typeand index is?. Does it change the situation/simplify it?I have an issue which probably has the same cause:
interface Todo { title: string, } function foo(x: {[name: string]: string}) { } function bar(todo: Partial<Todo>) { foo(todo); }When compiled with
--strictNullChecksit fails to compile with error:t.ts:9:9 - error TS2345: Argument of type 'Partial<Todo>' is not assignable to parameter of type '{ [name: string]: string; }'. Property 'title' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. 9 foo(todo); ~~~~ Found 1 error.Reacted by Daniel PfefferkornMartinJohns commented
on Jun 3, 2020 ContributorMore actionsStepan Koltsov (@stepancheg) Your type in
foosays it has arbitrary properties that are all of the typestring. But yourPartial<Todo>type has a property that isstring | undefined. You can't assignundefinedtostring.Martin Johns (@MartinJohns) then I don't understand how
Partialworks.So my problem can be written like this then:
interface Todo { title: string, } function foo(x: {[name: string]: string}) { } function bar<K extends keyof Todo>(todo: Pick<Todo, K>) { foo(todo); } function baz() { bar({}); bar({title: "a"}); // these fail as expected bar({title: 1}); bar({rrr: "a"}); }Thank you! And sorry for distraction.
- addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on May 21, 2022
TypeScript Version: 3.5.1
Search Terms:
Indexed, Partial, undefined, guard
Code
Expected behavior:
Inside the if statement,
valueshould have typeT[K]. Especially sincepartial[key]is assignable toT[K] | undefined, and in fact TypeScript reports the type in the error message asT[K] | undefined, inside the if statement! It's like it just doesn't simplifyPartial<T>[K]until it tries returning it.Here is the workaround, which doesn't involve any casting, just hints:
It would be nice if this hint wasn't necessary.
Actual behavior:
TypeScript complains that
T[K] | undefinedis not ok to return, even though the type should beT[K].Playground Link:
(requires strictNullChecks)
https://www.typescriptlang.org/play/#src=function%20getValue%3CT%2C%20K%20extends%20keyof%20T%3E(partial%3A%20Partial%3CT%3E%2C%20key%3A%20K)%3A%20T%5BK%5D%20%7C%20null%20%7B%0D%0A%20%20const%20value%20%3D%20partial%5Bkey%5D%0D%0A%20%20if%20(value%20!%3D%3D%20undefined)%20%7B%0D%0A%20%20%20%20return%20value%20%2F%2F%20error%3A%20TypeScript%20thinks%20this%20can%20be%20undefined...%0D%0A%20%20%7D%0D%0A%20%20return%20null%0D%0A%7D
Related Issues: