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

Object spread incorrectly introduces undefined value type in TS 4.2 #43045

Closed
danvk opened this issue Mar 2, 2021 · 4 comments · Fixed by #43086
Closed

Object spread incorrectly introduces undefined value type in TS 4.2 #43045

danvk opened this issue Mar 2, 2021 · 4 comments · Fixed by #43086
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.

Comments

@danvk
Copy link
Contributor

danvk commented Mar 2, 2021

Bug Report

This was the one type error I got when I updated my project from TS 4.1 → 4.2. It's a false positive so I figured I'd file an issue in case others run into it as well. I assume this is considered an acceptable loss from this change in the release notes, but it is a false positive and regression from 4.1 nonetheless!

🔎 Search Terms

  • object spread undefined 4.2

Possibly related to #13195

🕗 Version & Regression Information

  • This changed between versions 4.1.5 and 4.2.2

⏯ Playground Link

Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": 2,
    "target": "ES2017",
    "jsx": "React",
    "module": "ESNext"
  }
}

Playground Link: Provided

💻 Code

const param2 = Math.random() < 0.5 ? 'value2' : null;

const obj = {
    param1: 'value1',
    ...(param2 ? {param2} : {})
};

const query = Object.entries(obj).map(
    ([k, v]) => `${k}=${encodeURIComponent(v)}`
    //                                     ~
    // Argument of type 'string | undefined' is not assignable to parameter of type 'string | number | boolean'.
    //   Type 'undefined' is not assignable to type 'string | number | boolean'.(2345)
).join('&');
Output
"use strict";
const param2 = Math.random() < 0.5 ? 'value2' : null;
const obj = Object.assign({ param1: 'value1' }, (param2 ? { param2 } : {}));
const query = Object.entries(obj).map(([k, v]) => `${k}=${encodeURIComponent(v)}`
//                                     ~
// Argument of type 'string | undefined' is not assignable to parameter of type 'string | number | boolean'.
//   Type 'undefined' is not assignable to type 'string | number | boolean'.(2345)
).join('&');

🙁 Actual behavior

TypeScript says that v can be undefined, which it cannot. This leads to the type error in the call to encodeURIComponent.

🙂 Expected behavior

v can never be undefined at runtime, so the error is a false positive. This was not an error in TypeScript 4.1.

The root cause is that the type of obj is inferred as

const obj: {
    param2?: string | undefined;
    param1: string;
}

whereas in TS 4.1 the same inferred type shows up as:

const obj: {
    param2?: string;
    param1: string;
}
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Mar 4, 2021
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.3.1 milestone Mar 4, 2021
@ahejlsberg
Copy link
Member

The change in 4.2 to add undefined to the type of optional properties created by spread expressions is definitely correct. An optional property should always have undefined in its type.

The real issue is more likely that we should remove undefined from the type of optional properties when inferring from the properties of some object type to an index signature (as is the case in the Object.entries method).

@dwelle
Copy link

dwelle commented Mar 5, 2021

@ahejlsberg unclear whether the fix for "inferring to index signatures" covers e.g. this use case (React's setState):

type State = { a: number };
const setState = <K extends keyof State>(data: Pick<State, K>) => {};

setState({ ...({} as { a: 42 } | null) }); // error in 4.2.2

playground

@ahejlsberg
Copy link
Member

ahejlsberg commented Mar 5, 2021

@dwelle Unfortunately no. To cover that scenario we need the missing type such that optional properties can include missing, but not undefined, in their type.

@dwelle
Copy link

dwelle commented Mar 5, 2021

My bad, I thought TS < 4.2 allowed this, but it didn't. It just seems in our codebase we have a pattern that was using some weird loophole that was now fixed in 4.2:

type State = { a: number };
const setState = <K extends keyof State>(updater: () => Pick<State, K> | State | null) => {};

const obj = {} as { a: 42 } | null | undefined; // <-- both `null | undefined` is needed
setState(() => ({ ...obj })); // works in 4.1.5

Works in 4.1.5, but not in 4.2.2

But, seeing as it's working as intended, there's no reason to go back to the old "buggy" behavior I guess.

Any rough ETA on #13195?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants