Skip to content

Fix accidental null/undefined reduction when narrowing by null/undefined equality - #57692

Closed
Mateusz Burzyński (Andarist) wants to merge 2 commits into
microsoft:mainfrom
Andarist:fix/accidental-reduction-when-adjusting-facts
Closed

Mateusz Burzyński (Andarist) wants to merge 2 commits into
microsoft:mainfrom
Andarist:fix/accidental-reduction-when-adjusting-facts

Conversation

@Andarist

@Andarist Mateusz Burzyński (Andarist) commented Mar 8, 2024 •

Copy link
Copy Markdown
Contributor

fixes #57693

@typescript-bot TypeScript Bot (typescript-bot) added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Mar 8, 2024
@@ -0,0 +1,47 @@
narrowByUndefinedEqualityGeneric1.ts(21,23): error TS2345: Argument of type 'P[K] & ({} | null)' is not assignable to parameter of type 'S[K]'.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a separate issue but I'm somewhat confused by what happens here in terms of this error. Is this a separate bug? 🤔

It's especially confusing since this is OK:

type AnyObject = Record<string, any>;
type State = AnyObject;

declare function hasOwnProperty<T extends AnyObject>(
  object: T,
  prop: PropertyKey,
): prop is keyof T;

interface Store<S = State> {
  setState<K extends keyof S>(key: K, value: S[K]): void;
}

export function syncStoreProp<
  S extends State,
  P extends Partial<S>,
  K extends keyof S,
>(store: Store<S>, props: P, key: K) {
  const value = hasOwnProperty(props, key) ? props[key] : undefined;

  if (!value) return;
  // this is ok
  store.setState(key, value);
}

The difference is in the narrowed type of value - in the test case added here we end up with: P[K] & ({} | null) while the example above ends up being NonNullable<P[K]> (so basically P[K] & {}.

So the only difference is the null bit but even with that it's essentially (P[K] & null) | (P[K] & {}). We can see here that each member of this union is of type P[K] (with undefined already excluded). We can also reason that each P[K] is assignable to S[K] - as long as P[K] is not undefined - but we have already excluded that possibility.

There is also a question here if P[K] should even be allowed here since this is a valid call:

syncStoreProp({} as Store<{ a: string; b: number }>, { a: "foo" }, "b");

And we don't even need the hasOwnProperty check before creating this type. TS is OK with just this:

const value = props[key]

And if that wouldn't be OK then the example above (the one that narrowed the type to P[K] & {}) shouldn't typecheck either, right?

Comment thread src/compiler/checker.ts
switch (facts) {
case TypeFacts.NEUndefined:
return mapType(reduced, t => hasTypeFacts(t, TypeFacts.EQUndefined) ? getIntersectionType([t, hasTypeFacts(t, TypeFacts.EQNull) && !maybeTypeOfKind(reduced, TypeFlags.Null) ? getUnionType([emptyObjectType, nullType]) : emptyObjectType]) : t);
return mapType(reduced, t => hasTypeFacts(t, TypeFacts.EQUndefined) ? getIntersectionType([t, hasTypeFacts(t, TypeFacts.EQNull) && (!maybeTypeOfKind(reduced, TypeFlags.Null) || maybeTypeOfKind(t, TypeFlags.Null)) ? getUnionType([emptyObjectType, nullType]) : emptyObjectType]) : t);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly Anders Hejlsberg (@ahejlsberg)'s comment here then this fix should be done elsewhere. While creating this I wasn't sure if "replacing" any with unknown would always be desirable when computing the constraint - IIRC that would fix this issue too. For reference, currently any is replaced by unknown in getConstraintFromTypeParameter here:
https://github.dev/microsoft/TypeScript/blob/3fca8c842a42bb43ba5a86dcce1b7fb0eae6c7a4/src/compiler/checker.ts#L15813-L15818

@Andarist

Copy link
Copy Markdown
Contributor Author

superseded by #57724

@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 16, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Milestone Bug PRs that fix a bug with a specific milestone

Projects

None yet

Development

Successfully merging this pull request may close these issues.

null gets accidentally eliminated when narrowing by undefined's equality

3 participants