Skip to content

Unable to use rest on generic that has been narrowed to an object #59872

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
G-Rath opened this issue Sep 5, 2024 · 5 comments
Closed

Unable to use rest on generic that has been narrowed to an object #59872

G-Rath opened this issue Sep 5, 2024 · 5 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@G-Rath
Copy link

G-Rath commented Sep 5, 2024

🔎 Search Terms

"Rest types may only be created from object types" (and similar keyword-based rephrasing/shortenings)

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about spread, object, rest
  • I was unable to test this on prior versions because bandwidth + I don't think this was introduced in a particular commit
  • I have confirmed it happens on the current nightly and 5.6.1-rc

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/PTAEDMCcHsFtQBYBckAcDOAuEBzAlkggK4BGAdAMZzCx4UzrThLAAqAnqgKYDK9eqFnnToiXdMABMANgAsARkkBYAFDgiAOwpI80DaHQIAhgBsT0AO4BhaKnYAeVqC4APJFw0ATdKGgkAVlzaAHwAFOhIRu6YoKwAlDFOAN6qoKCQXEhEkPpJoGQFEVFcoAC+ANyqpaqqVBoREBryoAC8oI7Obh7eBkiQeBo4oAA+oHmoRpDoXJAxEf2DZWEAbolxrcFjqaB44KChSJxcTKDLrS1tAOTzAziX6ykqaWkZWTljoO5uMWcV29UqbZ1Bp5ApkDINUqtU6VQFPdKZbK5T6uJAxS4AfixlwANPkChCkGVYX84cCieANJJoR1Ud0fDdFqNxpNprNegshqUVmsNlt4bt9oduCczgBCC6gS5+QLae7854It7Ir5o07E-41eHkj5gwllaHLWHbV5Ij6q9FYjG4-Hg8RE0kVIA

💻 Code

// from https://github.com/microsoft/TypeScript/issues/26412
function shallowCopy<T extends object>(state: T): T {
  return { ...state };
}

const fn1 = <T extends string | { parser: string }>(v: T) => {
  if (typeof v === 'string') {
    return { text: v };
  }

  const { ...rest } = v;

  return { text: '???', ...rest };
};

const fn2 = <T extends string | { parser: string }>(v: T) => {
  if (typeof v !== 'object') {
    return { text: v };
  }

  const { ...rest } = v;

  return { text: '???', ...rest };
};

🙁 Actual behavior

The middle function has the error "Rest types may only be created from object types"

🙂 Expected behavior

No errors in any of the functions

Additional information about the issue

This looks like #26412 with just an extra twist - I figure (and hope) it's fixable since it works with the inverse !== 'object' check.

For a more real-world code example, I came across this when exploring a potential refactor to this section of code where I'd use spread/rest rather than a loop (so the proposed next line after that condition is const { a, b, c, ...remainingOptions } = config;, which triggers this)

@RyanCavanaugh
Copy link
Member

The error is correct; these narrowings are not equivalent. Knowing that T isn't string isn't sufficient proof that it's object. Consider something like this:

const fn1 = <T extends string | { toFixed(): string }>(v: T) => {
  if (typeof v === 'string') {
    return { text: v };
  }

  const { ...rest } = v;

  return { text: '???', ...rest };
};
fn1(42);

@G-Rath
Copy link
Author

G-Rath commented Sep 5, 2024

Right, so effectively this is me forgetting that this is "types on a dynamic runtime" rather than Go or Rust or whatever where it could only be those two types - my general thinking was it sufficient as I'd told TS its either "x or y" and then checked if it was "y" so the opposite branch should then be "x"...

I'm trying to think why that trips me up though - I feel like TS does behave like this elsewhere, but maybe I'm just misremembering some subtilties 🤔

(also I realised I've left out my truthy check which'd mean the whole "null is an object" thing in theory shouldn't be relevant - I'm pretty sure that doesn't matter for my simple example, but would for more real-world code)

@G-Rath
Copy link
Author

G-Rath commented Sep 5, 2024

actually maybe this is more about TS not having a difference between "pure data type object thing" and ... idk what to call the alternative - things with methods.? it's not that though but just i.e. in my head I'm thinking "this function takes a plain object with some properties", but you've shown TS actually checks for something slightly different meaning depending on your type something like a primitive could be considered valid for what looks like an object type, and then boom here we are...

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 5, 2024

If you can write "some string".length then string has a length property, so a string is a { length: number } even though it's not an object with a length property. You can try to slice this some other way but it's not clear that it's more intuitive.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Sep 5, 2024
@G-Rath
Copy link
Author

G-Rath commented Sep 6, 2024

That makes sense - it's that an "object" is different from "a value with properties", and that really TS's syntax describes the latter rather than the former (which makes sense because that's the underlying principle of duck typing, which is what JS is doing).

Happy for this to be closed, and thanks for working this through with me 🙂

@G-Rath G-Rath closed this as not planned Won't fix, can't repro, duplicate, stale Sep 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

2 participants