Skip to content

Add undefined to default-initialised parameters #12033

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

Merged
merged 28 commits into from
Feb 13, 2017

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Nov 3, 2016

Fixes #11900

Fixes #11424
Fixes #12726

When --strictNullChecks is on
When --strictNullChecks is on
Also update a baseline that changes now.
@sandersn sandersn changed the title Add undefined to default-valued parameters Add undefined to default-initialised parameters Nov 3, 2016
@@ -128,11 +128,11 @@ interface Foo {
declare function test1(x: Foo): void;
declare class Bar {
d: number;
e: number;
e: number | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

This is incorrect -- because there was a non-undefined initializer, the property should not include undefined

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

They are actually properties, so don't get undefined added when they
have an initialiser.

Also update baselines
@sandersn
Copy link
Member Author

sandersn commented Dec 8, 2016

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.

@sandersn
Copy link
Member Author

sandersn commented Dec 9, 2016

This is not the right fix. The behaviour is correct today -- you can pass undefined to a default-initialised parameter that's not last, and the compiler accepts it. And inside the body undefined is not part of the parameter. The correct fix is either to write ? in the signature, or not to change anything.

@sandersn sandersn closed this Dec 9, 2016
@sandersn
Copy link
Member Author

Actually, this is the right fix. It just needs to improve control flow to remove the added | undefined in the body of the function. I'll add the commits for that as soon as I get the .d.ts output corrected.

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

That seems like a good idea. Two notes though:

  1. We must not have very much declaration coverage because only two other tests broke.
  2. 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).

@sandersn
Copy link
Member Author

sandersn commented Jan 6, 2017

@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.
@ahejlsberg
Copy link
Member

I don't think this PR is the right fix. The type of a parameter that has an initializer should only include undefined when viewed as part of a parameter list. Within the actual function we don't want undefined in the type--as demonstrated by the PR it is a lot of work to then get rid of the undefined. We're already adding undefined to the type in getTypeOfParameter, why not just do the same when we display the function signature? Everything else already works correctly best I can tell.

@sandersn
Copy link
Member Author

@ahejlsberg The simpler fix doesn't take care of the narrowing an explicit undefined in an initialised parameter. Compare foo2 to foo1 and foo3 in the tests. All 3 have the correct signatures, but foo2 has x: string | undefined in its body even though it's guaranteed to be string.

    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.
@sandersn
Copy link
Member Author

OK, the simplified version is up now.

@ahejlsberg
Copy link
Member

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 undefined to an explicit type annotation when a parameter has a ? modifier, we should remove undefined from the type when the parameter has an initializer with a type that doesn't include undefined. The undefined would later be added back by getTypeOfParameter as appropriate. It looks to be a fairly simple change in getTypeForVariableLikeDeclaration.

@sandersn
Copy link
Member Author

OK, undefined is now removed when a parameter has a non-undefined-containing initializer.

// 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) {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understand the func && !func.body check. Also, where do you check if the binding pattern has a default value?

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

@@ -2694,7 +2694,11 @@ namespace ts {
writePunctuation(writer, SyntaxKind.ColonToken);
writeSpace(writer);

buildTypeDisplay(getTypeOfSymbol(p), writer, enclosingDeclaration, flags, symbolStack);
let type = getTypeOfSymbol(p);
if (strictNullChecks && parameterNode.initializer && !(getModifierFlags(parameterNode) & ModifierFlags.ParameterPropertyModifier)) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe turn this into a helper function. You have the same logic duplicated in writeTypeOfDeclaration.

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) ||
Copy link
Member

Choose a reason for hiding this comment

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

Could this be moved into writeTypeOfDeclaration. Seems like you're having to do this twice.

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

@@ -36,11 +36,11 @@ function f2(_a) {
declare function f1({a, b}?: {
a: number;
b: string;
}): void;
} | undefined): void;
Copy link
Member

Choose a reason for hiding this comment

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

Seems sort of redundant to include undefined when we already have a ? on the parameter. Same in a number of cases below.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

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
Copy link
Member

@ahejlsberg ahejlsberg left a comment

Choose a reason for hiding this comment

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

Approved with the noted changes.

@@ -3163,7 +3167,8 @@ namespace ts {
/** Return the inferred type for a binding element */
function getTypeForBindingElement(declaration: BindingElement): Type {
const pattern = <BindingPattern>declaration.parent;
const parentType = getTypeForBindingElementParent(<VariableLikeDeclaration>pattern.parent);
let parentType = getTypeForBindingElementParent(<VariableLikeDeclaration>pattern.parent);
Copy link
Member

Choose a reason for hiding this comment

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

Change back to const.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

if (shouldUseResolverType) {
format |= TypeFormatFlags.AddUndefined;
}
resolver.writeTypeOfDeclaration(declaration, enclosingDeclaration, format, writer);
Copy link
Member

Choose a reason for hiding this comment

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

Use functional style:

const format = TypeFormatFlags.UseTypeOfFunction | TypeFormatFlags.UseTypeAliasValue |
    (shouldUseResolverType ? TypeFormatFlags.AddUndefined : 0);

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants