Skip to content

Function augmented types are enforced inconsistently #61554

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
PartMan7 opened this issue Apr 9, 2025 · 7 comments
Closed

Function augmented types are enforced inconsistently #61554

PartMan7 opened this issue Apr 9, 2025 · 7 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@PartMan7
Copy link

PartMan7 commented Apr 9, 2025

🔎 Search Terms

function, interface, augment, parentheses, inconsistent

🕗 Version & Regression Information

  • This is the behavior in every version I tried (all the way down to 3.3.3), and I reviewed the FAQ for entries about pretty much everything.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEBzCB7ARlC8DeAoe8AlgHYAuIMAZlGAgGICuxYphKxO+B85AzqQH4AXPH4wSiANxcAvrjm4w7fvAAqIfo2YBGeAF54lJizYcAFAEocM6UuIr1m4wCZ98M0eat27q9hkW0p4mPo6kWmAAzJbWtsqkahqkAIIwMCgA7hG6BjF6AHyxivGJ-KnpWS5uZnmF-oG4uGHZAHR8CQba0s0ubUluXU1JEZF9Kp3dSeWZre0Dk2VpM71zE40gAB4ADigwCf7SQA

💻 Code

declare global {
  interface Function {
    test?: string;
  }
}

const TestFunc1 = function () {};
const TestFunc2 = (function () {});
function TestFunc3() {};
const TestArrowFunc1 = () => {};
const TestArrowFunc2 = (() => {});

TestFunc1.test = 1;
TestFunc2.test = 1;
TestFunc3.test = 1;
TestArrowFunc1.test = 1;
TestArrowFunc2.test = 1;

export {};

🙁 Actual behavior

Only TestFunc2 and TestArrowFunc2 (the ones with ()) are flagged as errors. All other cases are perfectly fine according to TypeScript.

🙂 Expected behavior

All five of the .test = 1 lines should be flagged as errors, since 1 is not compatible with string | undefined.

Additional information about the issue

I've only found this issue with properties on functions. Others (String, Number) work correctly in all cases, and even Function works correctly when the value is wrapped with () - but wrapping it in parentheses doesn't seem to have any semantic differences here.


My use case is trying to augment functions (specifically React components) in a massive codebase while enforcing types on the metadata being passed.

const Component = () => {};
Component.__metadata = metadata; // Function has the type so I don't have to cast or check!

Because of this bug, I have to use this instead:

import type { Metadata } from 'path/to/metadata/type';

const Component = () => {};
Component.__metadata = metadata satisfies Metadata as Metadata;

which is poor DX on three fronts (frustrating to read/write, appears redundant but is needed, and introduces a required import on every use).


Looked at:

@jcalz
Copy link
Contributor

jcalz commented Apr 9, 2025

I guess #26368 didn't anticipate that conflicting stuff might be merged into Function?

@nmain
Copy link

nmain commented Apr 9, 2025

declare global {
  interface Function {
    // ...

This feels like the wrong solution to the problem. With your augmentation you're claiming that every function of any sort observed anywhere in your application might have a __metadata, and if it does, it will conform to a specific shape. But that's not true; only React components in your app can be expected to follow that.

@PartMan7
Copy link
Author

PartMan7 commented Apr 9, 2025

Thanks for the input! If there's a solution that wouldn't involve me changing 100K+ files of React components I'd certainly be interested, but besides that this is a fairly safe assumption (since the field we've chosen is specifically unused and will not be used by anything else).

In any case, whether or not this is the best approach for what I'm trying to do is less relevant here than why TS is blocking it inconsistently, which is why I opened the issue in the first place

@nmain
Copy link

nmain commented Apr 9, 2025

In any case, whether or not this is the best approach for what I'm trying to do is less relevant here than why TS is blocking it inconsistently, which is why I opened the issue in the first place

Agreed, my feedback wasn't on-point here. I apologize.

This isn't an augmentation problem; #26368 allows override of any function prototype property with an incompatible one, although it tracks that specific function as having the new type: Playground

function foo() {

}

foo.toString = 14;

var x = foo.toString // number

@RyanCavanaugh
Copy link
Member

This follows from the intended semantics from #26368:

  • consts initialized with (exactly) a function literal can have properties appended to them
  • These appended properties can only be made with exactly constName.subProp = expr
  • Those properties can fully shadow the prototype properties

This is the first report of anyone ever not liking this combo, and I'm not sure which one you're proposing we change.

A different possible workaround you can do is

(TestFunc1).test = 1;

since that skips the middle rule.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Apr 9, 2025
@PartMan7
Copy link
Author

I see, thanks for the workaround! Is there any documentation on the handbook for this behaviour with functions? I could see this tripping up other people since it's not the behaviour I would've expected, and since there's no documentation I also didn't end up thinking of the (func).test approach.

Regardless, thanks for everyone's time!

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 14, 2025
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

5 participants