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

Empty object type is not working #47486

Closed
Asaf-S opened this issue Jan 18, 2022 · 12 comments
Closed

Empty object type is not working #47486

Asaf-S opened this issue Jan 18, 2022 · 12 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@Asaf-S
Copy link

Asaf-S commented Jan 18, 2022

Bug Report

πŸ”Ž Search Terms

  • Empty object Typescript

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed all of the FAQ's entries.

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

const emptyObject: Record<string, never> = {};
console.log(emptyObject.id); // Should warn, but doesn't

const nonEmptyObject: Record<'a', unknown> = { a: 1 };
console.log(nonEmptyObject.id); // Warning: Property 'id' does not exist on type 'Record<"a", unknown>'

πŸ™ Actual behavior

I wrapped the Request type of the Express library, in order to change the default value of the Request.params property.

type IEmptyObject = Record<string, never>;
export interface ICustomRequest<P extends IParams = IEmptyObject> extends Request<P> {}
function API(req: ICustomRequest) {
  // Expected to get a warning, but didn't
  console.log(req.params.anyParam);
}
express.get("/route", API)

If I would give a non-empty type, then it works:

function API(req: ICustomRequest<{definedParam:string}>) {
  // Now I get a warning
  console.log(req.params.anyParam);
}

πŸ™‚ Expected behavior

I should have been warned.

@fatcerberus
Copy link

Record<string, never> doesn't actually represent an empty object, but rather an object where every property is type never. As never is the bottom type and models functions that throw exceptions, it's assignable to everything. You probably want type IEmptyObject = {} instead.

@Josh-Cena
Copy link
Contributor

Hmm, how is type IEmptyObject = {} any better than Record<string, never>? It means any non-nullish value, including numbers.

const a: IEmptyObject = 1;

And it's banned by ts-eslint:

Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>` 

Record<never, never> is a viable solution to prevent any property access, but apart from that, there's no good way to do this

@fatcerberus
Copy link

fatcerberus commented Jan 18, 2022

Well, it seemed like the intent was for the default type to be "empty object" for which {} came closest (I don't know why eslint thinks Record<string, never> is a good substitute, it's not). But yeah, there's no real way to express "object with no properties" in TS because the structural typing allows you to have more properties than are named in the type. While Record<string, never> prevents you from constructing an object with any properties (since never is an empty set), reading from such a record is unrestricted.

Maybe Record<string, undefined>?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jan 18, 2022
@RyanCavanaugh
Copy link
Member

Record<string, never> is just not a good type to write down and ts-eslint should not be suggesting it. It means "Any property you access on this will throw", which isn't really true of anything except a proxy. The value { } is assignable into it because a Record<string, T> is always allowed to have zero properties.

@Asaf-S
Copy link
Author

Asaf-S commented Jan 18, 2022

Thx for your comments guys. I guess that all of us can agree that there's no way to represent an object with no properties and that's a bug, since {a:1} which means "one more property than none" does warn when using the wrong property.

The debate on which option is closer to an empty object is important, but might deflect the focus from the real problem that needs fixing...

@RyanCavanaugh
Copy link
Member

I guess that all of us can agree that there's no way to represent an object with no properties and that's a bug

Well, no. TypeScript intentionally has structural subtyping.

The scenario of needing to be given an exactly empty object seems like the XY problem here. Do you mean to just write object ?

@fatcerberus
Copy link

fatcerberus commented Jan 18, 2022

since {a:1} which means "one more property than none" does warn when using the wrong property

If you mean accessing the wrong property: this is true of {}, also. If you mean which properties the object is allowed to have, then no, that’s not the caseβ€”you can still end up with more properties than just a. The excess property check only applies to directly-assigned object literals. That’s by design due to the structural subtyping, as Ryan says. All object types are open-ended.

If TS had exact types (#12936), you could represent a truly empty object with {}.

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.

@Asaf-S
Copy link
Author

Asaf-S commented Jan 21, 2022

Hey guys, thank you for your answers. I have no problem of course with structural subtyping, I just wanted to get warned when accessing a property that was not defined on the object, which in my case needed to be empty. The object and {} types do cause that warning, and so they are a great fit for my use case. I think that ES-Lint's warning not to use them - was the thing that confused me.

How would you change ES-Lint's warning?

@Josh-Cena
Copy link
Contributor

@Asaf-S If you want to change the default error message, you can open a request in their repo: https://github.com/typescript-eslint/typescript-eslint and ask for their opinions about making the error message less confusing/ambiguous.

If you just want to change the message for yourself or unban it, you can use the rule's option: https://typescript-eslint.io/rules/ban-types#options

See also typescript-eslint/typescript-eslint#2063 (comment)

@fatcerberus
Copy link

Someone needs to let the ESLint team know that being able to assign primitives to {} is not actually unique to {}, e.g. the following is also valid:

const x: { toString(): string } = 42;

The only thing unique about {} as an object type, as far as I know, is that it turns off excess property checks.

@Josh-Cena
Copy link
Contributor

Josh-Cena commented Jan 21, 2022

I assume it's for the sake of "common mistakes". People are far more likely to write {} than to write something like {toString(): string} without knowing what they are doing.

The problem with {} is that it effectively means an "empty interface". Any property, on the instance or on its prototype, is allowed. It does forbid accessing properties because, well, it has none.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants