GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Property assignments in Typescript by sandersn · Pull Request #26368 · microsoft/TypeScript

Conversation

@sandersn

@sandersn Nathan Shively-Sanders (sandersn) commented Aug 10, 2018

Copy link
Copy Markdown
Member

Allow JS-container (expando) property assignments in Typescript, but only for function declarations and function/arrow expressions used as initialisers of const variables.

For example, now you can write the same code as you would in Javascript to make a callable object that also has properties:

function borscht(ingredients: any[]) {
   // let's just skip straight to the ???
}
borscht.and = 'burgers'

Previously you need to merge a namespace:

function borscht(ingredients: any[]) {
   // let's just skip straight to the ???
}
namespace borscht {
  export var and = 'burgers'
}

Note that this only works in the value space. To specify a callable object with properties, you should still use object literal notation (or merge a namespace):

declare const borscht = {
  (ingredients: any[]): Food
  and: string
}

Fixes #15868

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

levenleven, ulrichb, IllusionMH, benjaminjackman, bradleyayers, ryall, danielearwicker, miyaokamarina, schneiderfelipe, and OnesAndZer0s reacted with thumbs up emoji lencioni, brieb, mrmlnc, oriSomething, benjaminjackman, sean-vieira, bcherny, Yogu, miyaokamarina, and schneiderfelipe reacted with hooray emoji
All reactions
  • 👍 10 reactions
  • 🎉 10 reactions
Nathan Shively-Sanders (sandersn) added 5 commits August 8, 2018 15:39
@sandersn
Allow special property assignments in TS
bfcf324
But only for functions and constant variable declarations initialised with
functions.

This specifically excludes class declarations and class expressions,
which differs from Javascript. That's because Typescript supports
`static` properties, which are equivalent to property assignments to a
class.
@sandersn
Improve contextual typing predicate
3309104
Don't think it's right yet, but probably closer?
@sandersn
More fixes.
b0fe52f
The code is still fantastically ugly, but everything works the way it
should.

Also update baselines, even where it is ill-advised.
@sandersn
Cleanup
3060706
@sandersn
Remove extra whitespace
872fbbf
sandersn
Nathan Shively-Sanders (sandersn) commented Aug 10, 2018
View reviewed changes
Comment thread src/compiler/checker.ts
exportedType.numberIndexInfo);
result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Propagate JSLiteral flag
return result;
function getInitializerTypeFromSpecialDeclarations(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression, special: SpecialPropertyAssignmentKind) {

@sandersn Nathan Shively-Sanders (sandersn) Aug 10, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInitializerTypeFromSpecialDeclarations is only called once, so I just made the caller check for undefined. You should turn on Ignore Whitespace to read this review.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
sandersn
Nathan Shively-Sanders (sandersn) commented Aug 10, 2018
View reviewed changes
Comment thread src/compiler/utilities.ts
if (!isInJavaScriptFile(expr) ||
expr.operatorToken.kind !== SyntaxKind.EqualsToken ||
const special = getSpecialPropertyAssignmentKindWorker(expr);
return special === SpecialPropertyAssignmentKind.Property || isInJavaScriptFile(expr) ? special : SpecialPropertyAssignmentKind.None;

@sandersn Nathan Shively-Sanders (sandersn) Aug 10, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to create another wrapper/worker pair, but I couldn't come up with a better way to special-case the isJS check just for property assignments. Any ideas?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@sandersn
Nathan Shively-Sanders (sandersn) requested review from a user, Daniel Rosenwasser (DanielRosenwasser) and Ryan Cavanaugh (RyanCavanaugh) August 10, 2018 21:26
@ghost

Deleted user (ghost) commented Aug 10, 2018

Copy link
Copy Markdown

Do we have an issue open for the following?

function f() {}
f.p = { x: "" };
const { p } = f;
if (1 + 1 < 2) {
    f.p = { x: 0 };
}
f.p.x = 0;
p.x.toUpperCase(); // No compile error, crash at runtime

A hole like that might be acceptable for JS expando objects, but I don't know about having this in .ts files.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ghost

Deleted user (ghost) commented Aug 10, 2018

Copy link
Copy Markdown

Completions will have to be updated to allow f.x as a new identifier location when f is a function. That could be a separate issue.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Aug 10, 2018

Copy link
Copy Markdown
Member Author

I opened #26372 to track the type hole you found.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

ghost
Deleted user (ghost) reviewed Aug 10, 2018
View reviewed changes
Comment thread src/compiler/binder.ts Outdated
const lhs = node.left as PropertyAccessEntityNameExpression;
// Class declarations in Typescript do not allow property declarations
const parentSymbol = lookupSymbolForPropertyAccess(lhs.expression);
if (!isInJavaScriptFile(node) && !isTSFunctionSymbol(parentSymbol)) {

@ghost Deleted user (ghost) Aug 10, 2018

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: In both places isTsFunctionSymbol is called, you just checked isInJavaScriptFile to be false. So maybe it could just be isFunctionSymbol.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/compiler/utilities.ts Outdated
return isBinaryExpression(binExp) && getSpecialPropertyAssignmentKind(binExp) !== SpecialPropertyAssignmentKind.None && getNameOfDeclaration(binExp) === name;
return isBinaryExpression(binExp) &&
getSpecialPropertyAssignmentKind(binExp) !== SpecialPropertyAssignmentKind.None &&
(isInJavaScriptFile(name) || !isPropertyAccessExpression(binExp.left) || binExp.left.expression.symbol) &&

@ghost Deleted user (ghost) Aug 10, 2018

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests seem to pass without this line -- what's this protecting against?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@sandersn Nathan Shively-Sanders (sandersn) Aug 10, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindAllReferences relies on this function, and perhaps other services do. Without this line, a few fourslash tests fail, pointing out that getSpecialPropertyAssignmentKind now recognizes any property assignment in TS files, even those that aren’t actually expando assignments.

The left side of the or restores the old behavior for JS, and the right side adds to it for TS expando assignments. However, the right side isn’t tested. I need to add a case similar to the existing JS ones.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
RyanCavanaugh
Ryan Cavanaugh (RyanCavanaugh) reviewed Aug 10, 2018
View reviewed changes
Comment thread src/compiler/checker.ts
if (isEmptyArrayLiteralType(type)) {
if (noImplicitAny) {
reportImplicitAnyError(expression, anyArrayType);
(resolvedSymbol || symbol).exports!.forEach((s, name) => {

@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) Aug 10, 2018

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you briefly convince me that symbol.exports is always defined?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@sandersn Nathan Shively-Sanders (sandersn) Aug 14, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside the containing if, symbol is the commonjs equivalent of export =: module.exports = { ... }. So it's guaranteed to have exports -- they are the contents of the module.exports = initializer, and we restricted the initializer's type with type.flags & TypeFlags.Object.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Nathan Shively-Sanders (sandersn) added 3 commits August 15, 2018 08:42
@sandersn
Some kind of fix to isAnyDeclarationName
f090caf
It's not done yet.

Specifically, in TS:
Special property assignments are supposed to be declaration sites (but not all
top-level assignments), and I think I
got them to be. (But not sure).

In JS:
Special property assignments are supposed to be declaration sites (but not all
top-level assignments), and I'm pretty sure ALL top-level assignments
have been declaration sites for some time. This is incorrect, and
probably means the predicate needs to be the same for both dialects.
@sandersn
Add fourslash and improve isAnyDeclarationName
0b953b3
Now JS behaves the same as TS.
@sandersn
Cleanup from PR comments
869dc69
ghost
Deleted user (ghost) approved these changes Aug 15, 2018
View reviewed changes
@sandersn
Merge branch 'master' into property-assignments-in-ts
05c2ffc
@sandersn
Nathan Shively-Sanders (sandersn) merged commit cc67ce1 into master Aug 15, 2018
@sandersn
Nathan Shively-Sanders (sandersn) deleted the property-assignments-in-ts branch August 15, 2018 22:38
@AlCalzone AlCalzone mentioned this pull request Aug 17, 2018
Object literal not recognized as namespace #26505
Closed
@ghost ghost mentioned this pull request Aug 23, 2018
Inconsistent errors when assigning to property with index signature on object vs function #26647
Closed
@kaleb

Kaleb Hornsby (kaleb) commented Oct 3, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

This does not work when the property is a well-known symbol.

function count(start = 0) {
  while (true) {
    yield start;
    start += 1;
  }
}
count[Symbol.iterator] = () => count();
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Oct 3, 2018

Copy link
Copy Markdown
Member Author

Kaleb Hornsby (@kaleb) that’s not part of the current design; it only applies to property accesses, not element accesses. And it doesn’t handle computed property names at all, even well-known symbols. Can you open a separate issue to track that?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bcherny

Boris Cherny (bcherny) commented Oct 14, 2018

Copy link
Copy Markdown

Should TSC verify that every branch leads to an assignment in this case?

type WarnUser = {
  (warning: string): void
  wasCalled: boolean
}

const warnUser: WarnUser = (warning: string) => {
  if (warnUser.wasCalled) {
    return
  }
  warnUser.wasCalled = true
  alert(warning)
}
if (Math.random() < .5) {
  warnUser.wasCalled = false
}
// No error
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Oct 15, 2018

Copy link
Copy Markdown
Member Author

Boris Cherny (@bcherny) Your example actually doesn't use control flow much, which means that we won't be able to issue a use-before-def error. Let's look at each use of wasCalled:

  1. if (warnUser.wasCalled) is inside a closure. It's going to use the declared type, which comes from type WarnUser. Notably, WarnUser.wasCalled doesn't contain undefined and isn't optional.
  2. warnUser.wasCalled is also inside a closure. It doesn't count as a declaration since it's not in the same scope as warnUser. It will also use the declared type.
  3. Initialisation inside an if. This counts as a declaration so of course won't warn.
    3a. Before the if. There aren't any assignments or ifs here but the declared type isn't optional and doesn't contain undefined, so no error.
    3b. After the if. There is an if with an assignment inside, but they don't change the declared type.

You can do two [interesting] things to change this:

  1. Remove the type annotation. The type of warnUser remains the same, but is now anonymous. The declared type is now based on checking warnUser's initialiser, which causes control flow to check warnUser.wasCalled = false, which it treats as an assignment. Now, as desired, (3a) and (3b) give a use-before-def error and you can get (3b) to be correct by adding an else-branch+assignment to the if.
  2. Make WarnUser.wasCalled either optional or contain undefined in the type. You'd think this would make it behave the same as with the type annotation, but it has the same control flow behaviour as the original example — you get the declared type almost all the time. The type contains undefined, though, so the checker forces you to narrow that out before using the boolean. It just doesn't give a use-before-def error.
DickvdBrink, SlurpTheo, and orzFly reacted with thumbs up emoji bcherny, orzFly, and mtsoltan reacted with heart emoji
All reactions
  • 👍 3 reactions
  • ❤️ 3 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bcherny

Boris Cherny (bcherny) commented Oct 16, 2018

Copy link
Copy Markdown

Nathan Shively-Sanders (@sandersn) Thanks for the super detailed explanation of how the typechecker thinks about this example!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@tiagovtristao Tiago Tristao (tiagovtristao) mentioned this pull request Sep 20, 2020
Fix class expression from being assignable if types don't match #40660
Merged
@jcalz Joe Calzaretta (jcalz) mentioned this pull request Oct 9, 2021
Const variables initialized with parenthesized arrow expression cannot get expando properties #46284
Open
@jcalz Joe Calzaretta (jcalz) mentioned this pull request Apr 9, 2025
Function augmented types are enforced inconsistently #61554
Closed
@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 21, 2025
@typescript-bot TypeScript Bot (typescript-bot) assigned Nathan Shively-Sanders (sandersn) Oct 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) RyanCavanaugh left review comments
@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) Awaiting requested review from DanielRosenwasser
+1 more reviewer
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@sandersn Nathan Shively-Sanders (sandersn)

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@sandersn @kaleb @bcherny @RyanCavanaugh
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.