Add undefined to default-initialised parameters#12033
Conversation
When --strictNullChecks is on
When --strictNullChecks is on Also update a baseline that changes now.
| declare class Bar { | ||
| d: number; | ||
| e: number; | ||
| e: number | undefined; |
There was a problem hiding this comment.
This is incorrect -- because there was a non-undefined initializer, the property should not include undefined
They are actually properties, so don't get undefined added when they have an initialiser. Also update baselines
|
As @blakeembrey points out in #12726, default parameters should only add undefined to the type for the signature. In the body of the function, the parameters should not add undefined. |
|
This is not the right fix. The behaviour is correct today -- you can pass |
|
Actually, this is the right fix. It just needs to improve control flow to remove the added |
Also a little code to emit them in declarations, but this doesn't work yet.
They don't narrow the parameter type, except to remove undefined, and only if the initialiser type doesn't include undefined.
src/compiler/declarationEmitter.ts
Outdated
| writeTypeOfDeclaration(node, node.type, getParameterDeclarationTypeVisibilityError); | ||
| // use the checker's type, not the declared type, for an initialized parameter (that isn't a parameter property) | ||
| const isInitializedParameter = node.initializer && !(getModifierFlags(node) & ModifierFlags.ParameterPropertyModifier); | ||
| const typeNode = isInitializedParameter ? undefined : node.type; |
There was a problem hiding this comment.
so we treat foo(x?: string = "") different from foo(x?:string) ?
i think with this the first will be foo(x?: string | undefined) and for the second foo(x?: string). i would say both should be the same.
There was a problem hiding this comment.
That seems like a good idea. Two notes though:
- We must not have very much declaration coverage because only two other tests broke.
- This change makes it clearer than ever that duplicate marking of optionality is the way to go with our current semantics (which ignores the missing-undefined vs provided-undefined distinction).
|
@mhegazy I think this is ready to go now if you want to take one more look. |
Related to adding undefined, though not strictly the same, this change adds '?' to unused IIFE parameters in quick info.
|
I don't think this PR is the right fix. The type of a parameter that has an initializer should only include |
|
@ahejlsberg The simpler fix doesn't take care of the narrowing an explicit function foo1(x: string = "string", b: number) {
x.length;
}
function foo2(x: string | undefined = "string", b: number) {
x.length; // ok, should be narrowed to string
~
!!! error TS2532: Object is possibly 'undefined'.
}
function foo3(x = "string", b: number) {
x.length; // ok, should be narrowed to string
}However, the code is so much simpler that I don't mind missing this case. I'll commit the simplification so you can see what it looks like. |
Just add undefined when displaying the type. Don't actually add it to the type.
|
OK, the simplified version is up now. |
|
Regarding this error case function foo2(x: string | undefined = "string", b: number) {
x.length; // ok, should be narrowed to string
~
!!! error TS2532: Object is possibly 'undefined'.
}Similarly to how we add |
|
OK, |
src/compiler/checker.ts
Outdated
| // In strict null checking mode, a default value of a binding pattern adds undefined, | ||
| // which should be removed to get the type of the elements | ||
| const func = getContainingFunction(declaration); | ||
| if (strictNullChecks && func && !func.body && getFalsyFlags(parentType) & TypeFlags.Undefined) { |
There was a problem hiding this comment.
Not sure I understand the func && !func.body check. Also, where do you check if the binding pattern has a default value?
There was a problem hiding this comment.
Removal of undefined only applies to calls from declarationEmitter. But your other suggestion below (get rid of undefined for optional parameters) removes the need for this code entirely, so I'll just delete it.
src/compiler/checker.ts
Outdated
|
|
||
| buildTypeDisplay(getTypeOfSymbol(p), writer, enclosingDeclaration, flags, symbolStack); | ||
| let type = getTypeOfSymbol(p); | ||
| if (strictNullChecks && parameterNode.initializer && !(getModifierFlags(parameterNode) & ModifierFlags.ParameterPropertyModifier)) { |
There was a problem hiding this comment.
Maybe turn this into a helper function. You have the same logic duplicated in writeTypeOfDeclaration.
src/compiler/declarationEmitter.ts
Outdated
| writeTypeOfDeclaration(node, node.type, getParameterDeclarationTypeVisibilityError); | ||
| // use the checker's type, not the declared type, | ||
| // for optional parameters and initialized ones that aren't a parameter property | ||
| const typeShouldAddUndefined = resolver.isOptionalParameter(node) || |
There was a problem hiding this comment.
Could this be moved into writeTypeOfDeclaration. Seems like you're having to do this twice.
There was a problem hiding this comment.
In declarationEmitter, writeTypeOfDeclaration decides whether to call its own emitType or the checker's writeTypeOfDeclaration. I poked around and tried a few things, and I ended up creating a new TypeFormatFlags entry called AddUndefined.
| a: number; | ||
| b: string; | ||
| }): void; | ||
| } | undefined): void; |
There was a problem hiding this comment.
Seems sort of redundant to include undefined when we already have a ? on the parameter. Same in a number of cases below.
1. Add undefined only when an initialized parameter is required (not optional). 2. Create isRequiredInitializedParameter helper function 3. Call this function only once from declarationEmitter
ahejlsberg
left a comment
There was a problem hiding this comment.
Approved with the noted changes.
src/compiler/checker.ts
Outdated
| function getTypeForBindingElement(declaration: BindingElement): Type { | ||
| const pattern = <BindingPattern>declaration.parent; | ||
| const parentType = getTypeForBindingElementParent(<VariableLikeDeclaration>pattern.parent); | ||
| let parentType = getTypeForBindingElementParent(<VariableLikeDeclaration>pattern.parent); |
| if (shouldUseResolverType) { | ||
| format |= TypeFormatFlags.AddUndefined; | ||
| } | ||
| resolver.writeTypeOfDeclaration(declaration, enclosingDeclaration, format, writer); |
There was a problem hiding this comment.
Use functional style:
const format = TypeFormatFlags.UseTypeOfFunction | TypeFormatFlags.UseTypeAliasValue |
(shouldUseResolverType ? TypeFormatFlags.AddUndefined : 0);
Fixes #11900
Fixes #11424
Fixes #12726