Skip to content

fix(eslint-plugin): [ban-types] Suggest using object to mean "any object" #5018

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

Closed
wants to merge 1 commit into from

Conversation

RyanCavanaugh
Copy link

People vexed by

declare let j: unknown;
let m = { ...(j as {}) };

being banned by ban-types's default of disallowing {} have resorted to using

let m = { ...(j as never) };

which TS 4.7 flags as an error, for good reason. The alternatives suggested by this rule are all inappropriate for the situation:

// Doesn't fix anything
let m2 = { ...(j as unknown) };

// Adds undesirable index signature to result type
let m3 = { ...(j as Record<string, never>) };

// Adds undesirable index signature to result type
let m4 = { ...(j as Record<string, unknown>) };

The "correct" fix is

let m = { ...(j as object) };

Now that object has been shipping in TS for several years, this seems like a safe suggestion in lieu of Record's messy side effects

PR Checklist

Overview

People vexed by
```ts
declare let j: unknown;
let m = { ...(j as {}) };
```
being banned by `ban-types`'s default of disallowing `{}` have resorted to using

```ts
let m = { ...(j as never) };
```
which TS 4.7 flags as an error, for good reason. The alternatives suggested by this rule are all inappropriate for the situation:
```ts
// Doesn't fix anything
let m2 = { ...(j as unknown) };

// Adds undesirable index signature to result type
let m3 = { ...(j as Record<string, never>) };

// Adds undesirable index signature to result type
let m4 = { ...(j as Record<string, unknown>) };
```
The "correct" fix is
```ts
let m = { ...(j as object) };
```

Now that `object` has been shipping in TS for several years, this seems like a safe suggestion in lieu of `Record`'s messy side effects
@nx-cloud
Copy link

nx-cloud bot commented May 19, 2022

☁️ Nx Cloud Report

CI is running/has finished running commands for commit a4c9e0c. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this branch


✅ Successfully ran 47 targets

Sent with 💌 from NxCloud.

@netlify
Copy link

netlify bot commented May 19, 2022

👷 Deploy request for typescript-eslint pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit a4c9e0c

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @RyanCavanaugh!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint. As a thank you, your profile/company logo will be added to our main README which receives thousands of unique visitors per day.

@codecov
Copy link

codecov bot commented May 19, 2022

Codecov Report

Merging #5018 (a4c9e0c) into main (7e7b24c) will increase coverage by 2.46%.
The diff coverage is n/a.

@@            Coverage Diff             @@
##             main    #5018      +/-   ##
==========================================
+ Coverage   91.32%   93.79%   +2.46%     
==========================================
  Files         132      286     +154     
  Lines        1487     9795    +8308     
  Branches      224     2930    +2706     
==========================================
+ Hits         1358     9187    +7829     
- Misses         65      328     +263     
- Partials       64      280     +216     
Flag Coverage Δ
unittest 93.79% <ø> (+2.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
packages/eslint-plugin/src/rules/ban-types.ts 100.00% <ø> (ø)
packages/eslint-plugin/src/rules/no-namespace.ts 100.00% <0.00%> (ø)
...n/src/rules/indent-new-do-not-use/OffsetStorage.ts 100.00% <0.00%> (ø)
...lugin/src/rules/no-confusing-non-null-assertion.ts 82.60% <0.00%> (ø)
...ackages/eslint-plugin/src/rules/no-var-requires.ts 90.00% <0.00%> (ø)
...es/eslint-plugin/src/rules/no-loss-of-precision.ts 83.33% <0.00%> (ø)
...nt-plugin/src/rules/indent-new-do-not-use/index.ts 98.09% <0.00%> (ø)
...eslint-plugin/src/rules/prefer-return-this-type.ts 91.80% <0.00%> (ø)
...plugin/src/rules/naming-convention-utils/shared.ts 100.00% <0.00%> (ø)
packages/eslint-plugin/src/rules/no-unused-vars.ts 95.90% <0.00%> (ø)
... and 145 more

@bradzacher
Copy link
Member

bradzacher commented May 19, 2022

The issue with object as a suggested replacement has always been that it's really hard use (microsoft/TypeScript#21732).

Once you've got an object, you can't really do anything with it:

function foo(arg: object) {
    arg.foo; // can't access random properties
    
    if ('foo' in arg) {
        arg.foo; // can't do guarded random property access either
    }
}

repl

So as a "type meaning "any object"" replacement - object is not great!

OTOH Record<string, unknown> allows you to operate on the variable as an object and do everything one might expect of an object - so for the general case it's much more usable:

function foo(arg: Record<string, unknown>) {
    arg.foo; // can access random properties
    
    if ('foo' in arg) {
        arg.foo; // can do guarded random property access either
    }
}

repl

Really once you've got an object, you need some form of a user-defined type guard to get away from object - and most cases that type guard will convert it to a Record<string, unknown> anyways.

@RyanCavanaugh
Copy link
Author

RyanCavanaugh commented May 19, 2022

I don't feel like you addressed the scenario (spread) covered in the OP. All of the workarounds make the resultant type less safe by adding an index signature to m that allows arbitrary reads/writes, and you said the entire point of the default of {} was about not creating big type holes

@bradzacher
Copy link
Member

Is it common that people want to spread an unknown?
I personally haven't seen too many codebases attempt such an operation, but I admittedly am not exposed to a large spread of other codebases now-a-days.


Definitely correct that it's technically a safety hole in of itself.
Ideally the suggestion would be Readonly<Record<string, unknown>> - but people get angry when you suggest long types like that without a fixer - generally leading people to just use VSCode's quick fix disable instead.
Also from experience across TS codebases people tend to not use readonly too much and instead rely on implicit assumptions that thou shalt not mutate this.


I'd be more than happy if you want to specifically mention the spread case in the error message to help inform people of the right way to do things!
But I don't know if using object as the defacto "any object" replacement is necessarily the best thing to do.

@RyanCavanaugh
Copy link
Author

RyanCavanaugh commented May 20, 2022

The reason I bring this up is we found some new breaks (e.g. https://github.com/facebook/docusaurus/blob/c16a08cba59bea5e48d3a79cee82c23aaae29876/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/index.tsx#L26 ) due to TS 4.7 treating a spread of never as an error (since, in reality, we have no idea what's about to happen). The best assertion here is probably object (which used to be banned by ban-types, which I'm guessing is why the author didn't use that), with the second-best being { }, which is also banned. So instead they asserted to never, which is a) super wrong in code that actually executes and b) now broken.

If people want an object, meaning something whose proto is at least Object.prototype, then object really is the best fit for that. The various Record things here are distant seconds and thirds IMO. It feels like the rule didn't recommend object due to its old default configuration also banning the use of object, which I was glad to see removed.

Sort of an aside and I'll likely be opening a new issue: There's just a lot of confusion likely to come down the road on this specific ban in general, since we're likely to change the definition of NonNullable<T> to T & { } in 4.8, and are already in 4.8 going to change the default narrowing rules such that a truthy narrowing of a value of an unconstrained type parameter becomes T & { }. Having typescript-eslint, out of the box, ban you from writing a type that TypeScript itself is inferring under totally normal operation, is awkward.

The rest of ban-types is totally solid and I'd really raise reconsidering { } being in the default ruleset for ban-types, since the other bans it defaults to (String, Object, etc) are extremely well-grounded.

@Josh-Cena
Copy link
Member

FWIW, props as object doesn't fix the spreading props problem either.

I'm ambivalent on banning {}. It's useful for people who understand what it does, but it doesn't follow new users' intuitions (although there's nothing unsound about it; you can write { length: number } and assign a string to it as well).

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants