From 4c19873b648ee6bfe011ec1dd5fb4a33b3c75b89 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 29 Jun 2021 14:53:07 -0700 Subject: [PATCH] Validate symbol-named properties against symbol index signatures (#44815) * Validate symbols against both symbol and string index signatures * Add tests * Accept new baselines --- src/compiler/checker.ts | 6 ++- .../reference/indexSignatures1.errors.txt | 21 ++++++++- tests/baselines/reference/indexSignatures1.js | 33 ++++++++++++++ .../reference/indexSignatures1.symbols | 39 ++++++++++++++++ .../reference/indexSignatures1.types | 44 +++++++++++++++++++ .../types/members/indexSignatures1.ts | 14 ++++++ 6 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index eb00c3708d1..b64606d63b9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -12106,7 +12106,7 @@ namespace ts { } function getApplicableIndexInfoForName(type: Type, name: __String): IndexInfo | undefined { - return getApplicableIndexInfo(type, getStringLiteralType(unescapeLeadingUnderscores(name))); + return getApplicableIndexInfo(type, isLateBoundName(name) ? esSymbolType : getStringLiteralType(unescapeLeadingUnderscores(name))); } // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual @@ -27121,8 +27121,12 @@ namespace ts { */ function isKnownProperty(targetType: Type, name: __String, isComparingJsxAttributes: boolean): boolean { if (targetType.flags & TypeFlags.Object) { + // For backwards compatibility a symbol-named property is satisfied by a string index signature. This + // is incorrect and inconsistent with element access expressions, where it is an error, so eventually + // we should remove this exception. if (getPropertyOfObjectType(targetType, name) || getApplicableIndexInfoForName(targetType, name) || + isLateBoundName(name) && getIndexInfoOfType(targetType, stringType) || isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) { // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. return true; diff --git a/tests/baselines/reference/indexSignatures1.errors.txt b/tests/baselines/reference/indexSignatures1.errors.txt index 4d0310333c1..1a3ff537203 100644 --- a/tests/baselines/reference/indexSignatures1.errors.txt +++ b/tests/baselines/reference/indexSignatures1.errors.txt @@ -68,9 +68,11 @@ tests/cases/conformance/types/members/indexSignatures1.ts(281,35): error TS2322: Object literal may only specify known properties, and ''someKey'' does not exist in type 'PseudoDeclaration'. tests/cases/conformance/types/members/indexSignatures1.ts(286,7): error TS2322: Type '"two"' is not assignable to type '`/${string}`'. tests/cases/conformance/types/members/indexSignatures1.ts(289,7): error TS2322: Type 'number' is not assignable to type 'PathsObject'. +tests/cases/conformance/types/members/indexSignatures1.ts(312,43): error TS2322: Type '{ [sym]: string; }' is not assignable to type '{ [key: number]: string; }'. + Object literal may only specify known properties, and '[sym]' does not exist in type '{ [key: number]: string; }'. -==== tests/cases/conformance/types/members/indexSignatures1.ts (49 errors) ==== +==== tests/cases/conformance/types/members/indexSignatures1.ts (50 errors) ==== // Symbol index signature checking const sym = Symbol(); @@ -488,4 +490,21 @@ tests/cases/conformance/types/members/indexSignatures1.ts(289,7): error TS2322: const a: A = { [id]: 'test' } let aid = a[id]; + + // Repro from #44793 + + interface AA { + a?: string; + b?: number; + [key: symbol]: string; + } + + const aa: AA = { [sym]: '123' }; + + const obj1: { [key: symbol]: string } = { [sym]: 'hello '}; + const obj2: { [key: string]: string } = { [sym]: 'hello '}; // Permitted for backwards compatibility + const obj3: { [key: number]: string } = { [sym]: 'hello '}; // Error + ~~~~~~~~~~~~~~~ +!!! error TS2322: Type '{ [sym]: string; }' is not assignable to type '{ [key: number]: string; }'. +!!! error TS2322: Object literal may only specify known properties, and '[sym]' does not exist in type '{ [key: number]: string; }'. \ No newline at end of file diff --git a/tests/baselines/reference/indexSignatures1.js b/tests/baselines/reference/indexSignatures1.js index 1eb625374e6..0202d3df3ed 100644 --- a/tests/baselines/reference/indexSignatures1.js +++ b/tests/baselines/reference/indexSignatures1.js @@ -297,6 +297,20 @@ type A = Record; const a: A = { [id]: 'test' } let aid = a[id]; + +// Repro from #44793 + +interface AA { + a?: string; + b?: number; + [key: symbol]: string; +} + +const aa: AA = { [sym]: '123' }; + +const obj1: { [key: symbol]: string } = { [sym]: 'hello '}; +const obj2: { [key: string]: string } = { [sym]: 'hello '}; // Permitted for backwards compatibility +const obj3: { [key: number]: string } = { [sym]: 'hello '}; // Error //// [indexSignatures1.js] @@ -457,6 +471,10 @@ const pathObject = 123; // Error const id = '0000-0000-0000-0001'; const a = { [id]: 'test' }; let aid = a[id]; +const aa = { [sym]: '123' }; +const obj1 = { [sym]: 'hello ' }; +const obj2 = { [sym]: 'hello ' }; // Permitted for backwards compatibility +const obj3 = { [sym]: 'hello ' }; // Error //// [indexSignatures1.d.ts] @@ -627,3 +645,18 @@ declare const id: IdType; declare type A = Record; declare const a: A; declare let aid: string; +interface AA { + a?: string; + b?: number; + [key: symbol]: string; +} +declare const aa: AA; +declare const obj1: { + [key: symbol]: string; +}; +declare const obj2: { + [key: string]: string; +}; +declare const obj3: { + [key: number]: string; +}; diff --git a/tests/baselines/reference/indexSignatures1.symbols b/tests/baselines/reference/indexSignatures1.symbols index d2879177249..e5f0fea8150 100644 --- a/tests/baselines/reference/indexSignatures1.symbols +++ b/tests/baselines/reference/indexSignatures1.symbols @@ -860,3 +860,42 @@ let aid = a[id]; >a : Symbol(a, Decl(indexSignatures1.ts, 295, 5)) >id : Symbol(id, Decl(indexSignatures1.ts, 291, 5)) +// Repro from #44793 + +interface AA { +>AA : Symbol(AA, Decl(indexSignatures1.ts, 297, 16)) + + a?: string; +>a : Symbol(AA.a, Decl(indexSignatures1.ts, 301, 14)) + + b?: number; +>b : Symbol(AA.b, Decl(indexSignatures1.ts, 302, 15)) + + [key: symbol]: string; +>key : Symbol(key, Decl(indexSignatures1.ts, 304, 5)) +} + +const aa: AA = { [sym]: '123' }; +>aa : Symbol(aa, Decl(indexSignatures1.ts, 307, 5)) +>AA : Symbol(AA, Decl(indexSignatures1.ts, 297, 16)) +>[sym] : Symbol([sym], Decl(indexSignatures1.ts, 307, 16)) +>sym : Symbol(sym, Decl(indexSignatures1.ts, 2, 5)) + +const obj1: { [key: symbol]: string } = { [sym]: 'hello '}; +>obj1 : Symbol(obj1, Decl(indexSignatures1.ts, 309, 5)) +>key : Symbol(key, Decl(indexSignatures1.ts, 309, 15)) +>[sym] : Symbol([sym], Decl(indexSignatures1.ts, 309, 41)) +>sym : Symbol(sym, Decl(indexSignatures1.ts, 2, 5)) + +const obj2: { [key: string]: string } = { [sym]: 'hello '}; // Permitted for backwards compatibility +>obj2 : Symbol(obj2, Decl(indexSignatures1.ts, 310, 5)) +>key : Symbol(key, Decl(indexSignatures1.ts, 310, 15)) +>[sym] : Symbol([sym], Decl(indexSignatures1.ts, 310, 41)) +>sym : Symbol(sym, Decl(indexSignatures1.ts, 2, 5)) + +const obj3: { [key: number]: string } = { [sym]: 'hello '}; // Error +>obj3 : Symbol(obj3, Decl(indexSignatures1.ts, 311, 5)) +>key : Symbol(key, Decl(indexSignatures1.ts, 311, 15)) +>[sym] : Symbol([sym], Decl(indexSignatures1.ts, 311, 41)) +>sym : Symbol(sym, Decl(indexSignatures1.ts, 2, 5)) + diff --git a/tests/baselines/reference/indexSignatures1.types b/tests/baselines/reference/indexSignatures1.types index 1e73c76e244..b543739f238 100644 --- a/tests/baselines/reference/indexSignatures1.types +++ b/tests/baselines/reference/indexSignatures1.types @@ -1007,3 +1007,47 @@ let aid = a[id]; >a : A >id : `${number}-${number}-${number}-${number}` +// Repro from #44793 + +interface AA { + a?: string; +>a : string | undefined + + b?: number; +>b : number | undefined + + [key: symbol]: string; +>key : symbol +} + +const aa: AA = { [sym]: '123' }; +>aa : AA +>{ [sym]: '123' } : { [sym]: string; } +>[sym] : string +>sym : unique symbol +>'123' : "123" + +const obj1: { [key: symbol]: string } = { [sym]: 'hello '}; +>obj1 : { [key: symbol]: string; } +>key : symbol +>{ [sym]: 'hello '} : { [sym]: string; } +>[sym] : string +>sym : unique symbol +>'hello ' : "hello " + +const obj2: { [key: string]: string } = { [sym]: 'hello '}; // Permitted for backwards compatibility +>obj2 : { [key: string]: string; } +>key : string +>{ [sym]: 'hello '} : { [sym]: string; } +>[sym] : string +>sym : unique symbol +>'hello ' : "hello " + +const obj3: { [key: number]: string } = { [sym]: 'hello '}; // Error +>obj3 : { [key: number]: string; } +>key : number +>{ [sym]: 'hello '} : { [sym]: string; } +>[sym] : string +>sym : unique symbol +>'hello ' : "hello " + diff --git a/tests/cases/conformance/types/members/indexSignatures1.ts b/tests/cases/conformance/types/members/indexSignatures1.ts index a35471bdbfd..2d86c237681 100644 --- a/tests/cases/conformance/types/members/indexSignatures1.ts +++ b/tests/cases/conformance/types/members/indexSignatures1.ts @@ -300,3 +300,17 @@ type A = Record; const a: A = { [id]: 'test' } let aid = a[id]; + +// Repro from #44793 + +interface AA { + a?: string; + b?: number; + [key: symbol]: string; +} + +const aa: AA = { [sym]: '123' }; + +const obj1: { [key: symbol]: string } = { [sym]: 'hello '}; +const obj2: { [key: string]: string } = { [sym]: 'hello '}; // Permitted for backwards compatibility +const obj3: { [key: number]: string } = { [sym]: 'hello '}; // Error