microsoft/TypeScript

No excess properties error in nested intersection type

sktw opened this issue · 1 comments

sktw commented

TypeScript Version: 2.1.5

Code

interface A {
    x: string
}

interface B {
    a: A;
}

interface C {
    c: number;
}

type D = B & C;

let a: B = {a: {x: 'hello'}}; // ok
let b: B = {a: {x: 2}}; // error - types of property x are incompatible
let c: B = {a: {x: 'hello', y: 2}}; // error - y does not exist in type A

let d: D = {a: {x: 'hello'}, c: 5}; // ok
let e: D = {a: {x: 2}, c: 5}; // error - types of property x are incompatible
let f: D = {a: {x: 'hello', y: 2}, c: 5}; // should be an error?: y does not exist in type A

Expected behavior:
Excess property checking should cause a compiler error for the last example.

Actual behavior:
Last example compiles without error.

I just ran into this bug and it is marked "Needs Investigation", so I thought I would provide some. As far as I can tell (with the caveat that TypeScript is really complicated and I'm a newbie), the problem is with the following code in isRelatedTo in checker.ts (the file is too big to link to a formatted version):

                if (isObjectLiteralType(source) && source.flags & TypeFlags.FreshLiteral) {
                    const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
                    if (hasExcessProperties(<FreshObjectLiteralType>source, target, discriminantType, reportErrors)) {
                        if (reportErrors) {
                            reportRelationError(headMessage, source, target);
                        }
                        return Ternary.False;
                    }
                    // Above we check for excess properties with respect to the entire target type. When union
                    // and intersection types are further deconstructed on the target side, we don't want to
                    // make the check again (as it might fail for a partial target type). Therefore we obtain
                    // the regular source type and proceed with that.
                    if (isUnionOrIntersectionTypeWithoutNullableConstituents(target) && !discriminantType) {
                        source = getRegularTypeOfObjectLiteral(source);
                    }
                }

After the code checks for excess properties only at the top level, getRegularTypeOfObjectLiteral disables the check recursively. Indeed, if a source type S can be successfully compared to a target type X & Y or X | Y by comparing S to X and/or Y individually, at no point during the recursive comparison to X or Y do we have all the information to do a correct check for excess properties in object literal types nested in S. So the least disruptive way I can think of to fix the problem is to change the code quoted above to do a recursive check for excess properties. Another option would be to declare the problem a design limitation.