Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Mar 8, 2024

fixes #57693

@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
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?

@typescript-bot typescript-bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Mar 8, 2024
@@ -26969,9 +26969,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (strictNullChecks) {
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
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 @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
Contributor Author

superseded by #57724

@Andarist Andarist closed this Mar 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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