-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Property assignments in Typescript #26368
Conversation
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.
Don't think it's right yet, but probably closer?
The code is still fantastically ugly, but everything works the way it should. Also update baselines, even where it is ill-advised.
exportedType.numberIndexInfo); | ||
result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Propagate JSLiteral flag | ||
return result; | ||
function getInitializerTypeFromSpecialDeclarations(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression, special: SpecialPropertyAssignmentKind) { |
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.
if (!isInJavaScriptFile(expr) || | ||
expr.operatorToken.kind !== SyntaxKind.EqualsToken || | ||
const special = getSpecialPropertyAssignmentKindWorker(expr); | ||
return special === SpecialPropertyAssignmentKind.Property || isInJavaScriptFile(expr) ? special : SpecialPropertyAssignmentKind.None; |
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?
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 |
Completions will have to be updated to allow |
I opened #26372 to track the type hole you found. |
src/compiler/binder.ts
Outdated
@@ -2477,6 +2477,11 @@ namespace ts { | |||
|
|||
function bindSpecialPropertyAssignment(node: BinaryExpression) { | |||
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)) { |
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
.
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) && |
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?
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.
if (isEmptyArrayLiteralType(type)) { | ||
if (noImplicitAny) { | ||
reportImplicitAnyError(expression, anyArrayType); | ||
(resolvedSymbol || symbol).exports!.forEach((s, name) => { |
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?
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
.
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.
Now JS behaves the same as TS.
This does not work when the property is a well-known symbol.
|
@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? |
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 |
@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
You can do two [interesting] things to change this:
|
@sandersn Thanks for the super detailed explanation of how the typechecker thinks about this example! |
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:
Previously you need to merge a namespace:
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):
Fixes #15868