Skip to content

never-typed properties are required when instantiating object #54053

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
ryami333 opened this issue Apr 28, 2023 · 11 comments
Closed

never-typed properties are required when instantiating object #54053

ryami333 opened this issue Apr 28, 2023 · 11 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@ryami333
Copy link

ryami333 commented Apr 28, 2023

Bug Report

🔎 Search Terms

never, discriminated union

🕗 Version & Regression Information

I am seeing this bug in Typescript 5.0.4 (and in the latest beta at time of writing 5.1.0 Beta, and in the Nightly v5.1.0-dev20230428), but I do not know when it was introduced. I've also checked all 4.X releases in the Typescript Playground and it is indeed reproducible in all of them.

This is the behavior in every version I tried, and I reviewed the FAQ for entries about never.

⏯ Playground Link

Playground link with relevant code

💻 Code

type Person = { firstName: string, surname: string; tailLength: never };

const me: Person = {
  firstName: "Homer",
  surname: "Simpson"
}

🙁 Actual behavior

When instantiating me, the following type-error is produced:

Property 'tailLength' is missing in type '{ firstName: string; surname: string; }' but required in type 'Person'.

🙂 Expected behavior

I would expect that never-typed properties should not be "required" properties.


Additional Context

The Playground link offers a minimal-reproducible case, but does not contextualise a real-world use-case. Of course I could just remove the tailLength property from the Person-type, and that would unblock me from instantiating me without a tailLength property. My real-world use case involves discriminated union patterns. I've been trying to build this Button component, which can render either an <a /> or <button /> element, depending on the type property that I pass to it. Here's what I tried originally:

// Use-case Example #1

const Button = ({
  _type,
  href,
  onClick,
}:
  | { _type: "button"; onClick: React.MouseEventHandler }
  | { _type: "link"; href: string }) => {
  switch (_type) {
    case "button": {
      return <button className="button" onClick={onClick} />;
    }
    case "link": {
      return <a className="button" href={href} />;
    }
  }
};

However, this produces type errors on href and onClick, such as:

Property 'href' does not exist on type '{ _type: "button"; onClick: React.MouseEventHandler; } | { _type: "link"; href: string; }'.

Property 'onClick' does not exist on type '{ _type: "button"; onClick: React.MouseEventHandler; } | { _type: "link"; href: string; }'.

I can address these errors by making all props present on both sides of the type-union, using never:

// Use-case Example #2

const Button = ({
  _type,
  href,
  onClick,
}:
  | { _type: "button"; onClick: React.MouseEventHandler; href: never }
  | { _type: "link"; onClick: never; href: string }) => {
  switch (_type) {
    case "button": {
      return <button className="button" onClick={onClick} />;
    }
    case "link": {
      return <a className="button" href={href} />;
    }
  }
};

And this resolves the errors in the component definition, raises type errors at the call-location:

<Button type="link" href="https://www.typescriptlang.org/" />

Produces:

Type … is not assignable … Property 'onClick' is … required in type '{ _type: "link"; onClick: never; href: string; }'.

So, there is a circular issue here. In my opinion, making never-typed properties "required" basically reduces their utility, because (unless I'm mistaken) there is no valid value which you can pass (not even any!).

@MartinJohns
Copy link
Contributor

This is working as intended, see #37104.

And technically, a property typed never is a property that should throw an error upon access, not a property that doesn't exist. You can't represent the absence of properties in TypeScripts type system, this would require the "Exact Types" feature.

@ryami333
Copy link
Author

Yes I see where you're coming from - but I suppose my objection is that instantiation is, by definition, not access. Or am I mistaken there?

@MartinJohns
Copy link
Contributor

MartinJohns commented Apr 28, 2023

interface A {
  a: string;
  b: never;
}

This type says it has two properties: one typed string, and one that throws an error upon access. You can't just omit the second property, it's part of the interface definition, you must provide it when constructing an object:

const a: A = {
  a: "abc",
  get b(): never { throw new Error(); }
};

A property typed never is still a property that exists and must be defined. It does not mean it's a property that doesn't exist.

@Andarist
Copy link
Contributor

And technically, a property typed never is a property that should throw an error upon access,

Is this ever a thing? I can't repro this anyhow or I'm unsure what kind of access you have in mind:

interface A {
  a: string;
  b: never;
}

declare const foo: A
foo.b // ok
const str: string = foo.b // fine, never is assignable to everything

@fatcerberus
Copy link

That’s the point - never is assignable to everything because it’s uninhabited; you can’t produce a value of that type and must throw. If you don’t, the code isn’t sound.

@ryami333
Copy link
Author

ryami333 commented Apr 28, 2023

a property typed never is a property that should throw an error upon access

This doesn't really hold up as an objective truth, though. Plus, I think we're conflating thrown (runtime) errors and compile-time (type) errors, which are not the same thing.

const Button = ({
  _type,
  href,
  onClick,
}:
  | { _type: "button"; onClick: () => void; href: never }
  | { _type: "link"; onClick: never; href: string }) => {
  console.log(href);
};

The destructuring assignment operation is "access" - is it not? This code does not result in a thrown error, nor a raised type error.

I stand by my statement at the end of my original post - the utility of never types is handicapped by this behaviour - it would appear to my (admittedly uneducated) eye that the compiler needs to pick a lane: either it needs to let me omit the property upon instantiation, or there needs to be a valid value/type that one can pass to a never-typed property (which would be daft because it defeats the purpose).

@fatcerberus
Copy link

If you’re trying to ensure href doesn’t exist then you want to write href?: never.

@MartinJohns
Copy link
Contributor

Be aware that the solution by @fatcerberus does not guarantee mutually exclusive types either. It's always a possibility that both properties are defined.


And I essentially just repeated what Ryan said before:

#47071 (comment)

Record<string, never> describes a type that, if you accessed any of its properties, you'd get an exception.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label May 1, 2023
@microsoft-github-policy-service

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@microsoft-github-policy-service

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

1 similar comment
@microsoft-github-policy-service

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants