Skip to content

Commit fefc47f

Browse files
authored
Flag JS Literals and ignore assignments/accesses to invalid props, instead of adding an index (#25996)
* Remove index signatures from js literals, use an object flag to indicate errors should be ignored instead * Add focused test on the keyof problem * Fix fourslash test * Reenable errors with noImplicitAny flag * Also disable excess property checks outside of noImplicitAny mode for js literals * Edit and move comments
1 parent eb763f0 commit fefc47f

File tree

65 files changed

+526
-369
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+526
-369
lines changed

src/compiler/checker.ts

+56-7
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ namespace ts {
425425
const resolvingSignaturesArray = [resolvingSignature];
426426

427427
const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);
428-
const jsObjectLiteralIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false);
429428

430429
const globals = createSymbolTable();
431430
let amalgamatedDuplicates: Map<{ firstFile: SourceFile, secondFile: SourceFile, firstFileInstances: Map<{ instances: Node[], blockScoped: boolean }>, secondFileInstances: Map<{ instances: Node[], blockScoped: boolean }> }> | undefined;
@@ -4809,13 +4808,15 @@ namespace ts {
48094808
members.set(name, s);
48104809
}
48114810
});
4812-
return createAnonymousType(
4811+
const result = createAnonymousType(
48134812
exportedType.symbol,
48144813
members,
48154814
exportedType.callSignatures,
48164815
exportedType.constructSignatures,
48174816
exportedType.stringIndexInfo,
48184817
exportedType.numberIndexInfo);
4818+
result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Propagate JSLiteral flag
4819+
return result;
48194820
}
48204821
if (isEmptyArrayLiteralType(type)) {
48214822
if (noImplicitAny) {
@@ -5123,7 +5124,9 @@ namespace ts {
51235124
if (s && hasEntries(s.exports)) {
51245125
mergeSymbolTable(exports, s.exports);
51255126
}
5126-
return createAnonymousType(symbol, exports, emptyArray, emptyArray, jsObjectLiteralIndexInfo, undefined);
5127+
const type = createAnonymousType(symbol, exports, emptyArray, emptyArray, undefined, undefined);
5128+
type.objectFlags |= ObjectFlags.JSLiteral;
5129+
return type;
51275130
}
51285131
}
51295132

@@ -9074,6 +9077,34 @@ namespace ts {
90749077
return type;
90759078
}
90769079

9080+
/**
9081+
* Returns if a type is or consists of a JSLiteral object type
9082+
* In addition to objects which are directly literals,
9083+
* * unions where every element is a jsliteral
9084+
* * intersections where at least one element is a jsliteral
9085+
* * and instantiable types constrained to a jsliteral
9086+
* Should all count as literals and not print errors on access or assignment of possibly existing properties.
9087+
* This mirrors the behavior of the index signature propagation, to which this behaves similarly (but doesn't affect assignability or inference).
9088+
*/
9089+
function isJSLiteralType(type: Type): boolean {
9090+
if (noImplicitAny) {
9091+
return false; // Flag is meaningless under `noImplicitAny` mode
9092+
}
9093+
if (getObjectFlags(type) & ObjectFlags.JSLiteral) {
9094+
return true;
9095+
}
9096+
if (type.flags & TypeFlags.Union) {
9097+
return every((type as UnionType).types, isJSLiteralType);
9098+
}
9099+
if (type.flags & TypeFlags.Intersection) {
9100+
return some((type as IntersectionType).types, isJSLiteralType);
9101+
}
9102+
if (type.flags & TypeFlags.Instantiable) {
9103+
return isJSLiteralType(getResolvedBaseConstraint(type));
9104+
}
9105+
return false;
9106+
}
9107+
90779108
function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode | undefined, cacheSymbol: boolean) {
90789109
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined;
90799110
const propName = isTypeUsableAsLateBoundName(indexType) ? getLateBoundNameFromType(indexType) :
@@ -9122,6 +9153,9 @@ namespace ts {
91229153
if (indexType.flags & TypeFlags.Never) {
91239154
return neverType;
91249155
}
9156+
if (isJSLiteralType(objectType)) {
9157+
return anyType;
9158+
}
91259159
if (accessExpression && !isConstEnumObjectType(objectType)) {
91269160
if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors) {
91279161
if (getIndexTypeOfType(objectType, IndexKind.Number)) {
@@ -9142,6 +9176,9 @@ namespace ts {
91429176
return anyType;
91439177
}
91449178
}
9179+
if (isJSLiteralType(objectType)) {
9180+
return anyType;
9181+
}
91459182
if (accessNode) {
91469183
const indexNode = accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : accessNode.indexType;
91479184
if (indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
@@ -11217,6 +11254,9 @@ namespace ts {
1121711254
}
1121811255

1121911256
function hasExcessProperties(source: FreshObjectLiteralType, target: Type, discriminant: Type | undefined, reportErrors: boolean): boolean {
11257+
if (!noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
11258+
return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny
11259+
}
1122011260
if (maybeTypeOfKind(target, TypeFlags.Object) && !(getObjectFlags(target) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) {
1122111261
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
1122211262
if ((relation === assignableRelation || relation === definitelyAssignableRelation || relation === comparableRelation) &&
@@ -12705,7 +12745,7 @@ namespace ts {
1270512745
resolved.stringIndexInfo,
1270612746
resolved.numberIndexInfo);
1270712747
regularNew.flags = resolved.flags & ~TypeFlags.FreshLiteral;
12708-
regularNew.objectFlags |= ObjectFlags.ObjectLiteral;
12748+
regularNew.objectFlags |= ObjectFlags.ObjectLiteral | (getObjectFlags(resolved) & ObjectFlags.JSLiteral);
1270912749
(<FreshObjectLiteralType>type).regularType = regularNew;
1271012750
return regularNew;
1271112751
}
@@ -12784,9 +12824,11 @@ namespace ts {
1278412824
}
1278512825
const stringIndexInfo = getIndexInfoOfType(type, IndexKind.String);
1278612826
const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number);
12787-
return createAnonymousType(type.symbol, members, emptyArray, emptyArray,
12827+
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray,
1278812828
stringIndexInfo && createIndexInfo(getWidenedType(stringIndexInfo.type), stringIndexInfo.isReadonly),
1278912829
numberIndexInfo && createIndexInfo(getWidenedType(numberIndexInfo.type), numberIndexInfo.isReadonly));
12830+
result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Retain js literal flag through widening
12831+
return result;
1279012832
}
1279112833

1279212834
function getWidenedType(type: Type) {
@@ -16791,12 +16833,15 @@ namespace ts {
1679116833
return createObjectLiteralType();
1679216834

1679316835
function createObjectLiteralType() {
16794-
const stringIndexInfo = isJSObjectLiteral ? jsObjectLiteralIndexInfo : hasComputedStringProperty ? getObjectLiteralIndexInfo(node.properties, offset, propertiesArray, IndexKind.String) : undefined;
16836+
const stringIndexInfo = hasComputedStringProperty ? getObjectLiteralIndexInfo(node.properties, offset, propertiesArray, IndexKind.String) : undefined;
1679516837
const numberIndexInfo = hasComputedNumberProperty && !isJSObjectLiteral ? getObjectLiteralIndexInfo(node.properties, offset, propertiesArray, IndexKind.Number) : undefined;
1679616838
const result = createAnonymousType(node.symbol, propertiesTable, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);
1679716839
const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : TypeFlags.FreshLiteral;
1679816840
result.flags |= TypeFlags.ContainsObjectLiteral | freshObjectLiteralFlag | (typeFlags & TypeFlags.PropagatingFlags);
1679916841
result.objectFlags |= ObjectFlags.ObjectLiteral;
16842+
if (isJSObjectLiteral) {
16843+
result.objectFlags |= ObjectFlags.JSLiteral;
16844+
}
1680016845
if (patternWithComputedProperties) {
1680116846
result.objectFlags |= ObjectFlags.ObjectLiteralPatternWithComputedProperties;
1680216847
}
@@ -17872,6 +17917,9 @@ namespace ts {
1787217917
if (!prop) {
1787317918
const indexInfo = getIndexInfoOfType(apparentType, IndexKind.String);
1787417919
if (!(indexInfo && indexInfo.type)) {
17920+
if (isJSLiteralType(leftType)) {
17921+
return anyType;
17922+
}
1787517923
if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) {
1787617924
reportNonexistentProperty(right, leftType.flags & TypeFlags.TypeParameter && (leftType as TypeParameter).isThisType ? apparentType : leftType);
1787717925
}
@@ -20009,7 +20057,8 @@ namespace ts {
2000920057
if (decl) {
2001020058
const jsSymbol = getSymbolOfNode(decl);
2001120059
if (jsSymbol && hasEntries(jsSymbol.exports)) {
20012-
jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, jsObjectLiteralIndexInfo, undefined);
20060+
jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined);
20061+
(jsAssignmentType as ObjectType).objectFlags |= ObjectFlags.JSLiteral;
2001320062
}
2001420063
}
2001520064
}

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3824,6 +3824,7 @@ namespace ts {
38243824
ReverseMapped = 1 << 11, // Object contains a property from a reverse-mapped type
38253825
JsxAttributes = 1 << 12, // Jsx attributes type
38263826
MarkerType = 1 << 13, // Marker type used for variance probing
3827+
JSLiteral = 1 << 14, // Object type declared in JS - disables errors on read/write of nonexisting members
38273828
ClassOrInterface = Class | Interface
38283829
}
38293830

tests/baselines/reference/api/tsserverlibrary.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,7 @@ declare namespace ts {
22312231
ReverseMapped = 2048,
22322232
JsxAttributes = 4096,
22332233
MarkerType = 8192,
2234+
JSLiteral = 16384,
22342235
ClassOrInterface = 3
22352236
}
22362237
interface ObjectType extends Type {

tests/baselines/reference/api/typescript.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,7 @@ declare namespace ts {
22312231
ReverseMapped = 2048,
22322232
JsxAttributes = 4096,
22332233
MarkerType = 8192,
2234+
JSLiteral = 16384,
22342235
ClassOrInterface = 3
22352236
}
22362237
interface ObjectType extends Type {

tests/baselines/reference/chainedPrototypeAssignment.types

+13-13
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,30 @@ var mod = require('./mod');
77
>'./mod' : "./mod"
88

99
var a = new mod.A()
10-
>a : A & { [x: string]: any; m(n: number): number; }
11-
>new mod.A() : A & { [x: string]: any; m(n: number): number; }
10+
>a : A & { m(n: number): number; }
11+
>new mod.A() : A & { m(n: number): number; }
1212
>mod.A : typeof A
1313
>mod : typeof import("tests/cases/conformance/salsa/mod")
1414
>A : typeof A
1515

1616
var b = new mod.B()
17-
>b : B & { [x: string]: any; m(n: number): number; }
18-
>new mod.B() : B & { [x: string]: any; m(n: number): number; }
17+
>b : B & { m(n: number): number; }
18+
>new mod.B() : B & { m(n: number): number; }
1919
>mod.B : typeof B
2020
>mod : typeof import("tests/cases/conformance/salsa/mod")
2121
>B : typeof B
2222

2323
a.m('nope')
2424
>a.m('nope') : number
2525
>a.m : (n: number) => number
26-
>a : A & { [x: string]: any; m(n: number): number; }
26+
>a : A & { m(n: number): number; }
2727
>m : (n: number) => number
2828
>'nope' : "nope"
2929

3030
b.m('not really')
3131
>b.m('not really') : number
3232
>b.m : (n: number) => number
33-
>b : B & { [x: string]: any; m(n: number): number; }
33+
>b : B & { m(n: number): number; }
3434
>m : (n: number) => number
3535
>'not really' : "not really"
3636

@@ -81,15 +81,15 @@ exports.B = B
8181
>B : typeof B
8282

8383
A.prototype = B.prototype = {
84-
>A.prototype = B.prototype = { /** @param {number} n */ m(n) { return n + 1 }} : { [x: string]: any; m(n: number): number; }
85-
>A.prototype : { [x: string]: any; m(n: number): number; }
84+
>A.prototype = B.prototype = { /** @param {number} n */ m(n) { return n + 1 }} : { m(n: number): number; }
85+
>A.prototype : { m(n: number): number; }
8686
>A : typeof A
87-
>prototype : { [x: string]: any; m(n: number): number; }
88-
>B.prototype = { /** @param {number} n */ m(n) { return n + 1 }} : { [x: string]: any; m(n: number): number; }
89-
>B.prototype : { [x: string]: any; m(n: number): number; }
87+
>prototype : { m(n: number): number; }
88+
>B.prototype = { /** @param {number} n */ m(n) { return n + 1 }} : { m(n: number): number; }
89+
>B.prototype : { m(n: number): number; }
9090
>B : typeof B
91-
>prototype : { [x: string]: any; m(n: number): number; }
92-
>{ /** @param {number} n */ m(n) { return n + 1 }} : { [x: string]: any; m(n: number): number; }
91+
>prototype : { m(n: number): number; }
92+
>{ /** @param {number} n */ m(n) { return n + 1 }} : { m(n: number): number; }
9393

9494
/** @param {number} n */
9595
m(n) {

tests/baselines/reference/checkDestructuringShorthandAssigment.types

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
=== tests/cases/compiler/bug25434.js ===
22
// should not crash while checking
33
function Test({ b = '' } = {}) {}
4-
>Test : ({ b }?: { [x: string]: any; }) => void
4+
>Test : ({ b }?: {}) => void
55
>b : string
66
>'' : ""
77
>{} : { b?: string; }
88

99
Test(({ b = '5' } = {}));
1010
>Test(({ b = '5' } = {})) : void
11-
>Test : ({ b }?: { [x: string]: any; }) => void
11+
>Test : ({ b }?: {}) => void
1212
>({ b = '5' } = {}) : { b?: any; }
1313
>{ b = '5' } = {} : { b?: any; }
14-
>{ b = '5' } : { [x: string]: any; b?: any; }
14+
>{ b = '5' } : { b?: any; }
1515
>b : any
1616
>'5' : "5"
1717
>{} : { b?: any; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
tests/cases/compiler/file.js(11,1): error TS2322: Type '"z"' is not assignable to type '"x" | "y"'.
2+
3+
4+
==== tests/cases/compiler/file.js (1 errors) ====
5+
// @ts-check
6+
const obj = {
7+
x: 1,
8+
y: 2
9+
};
10+
11+
/**
12+
* @type {keyof typeof obj}
13+
*/
14+
let selected = "x";
15+
selected = "z"; // should fail
16+
~~~~~~~~
17+
!!! error TS2322: Type '"z"' is not assignable to type '"x" | "y"'.
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [file.js]
2+
// @ts-check
3+
const obj = {
4+
x: 1,
5+
y: 2
6+
};
7+
8+
/**
9+
* @type {keyof typeof obj}
10+
*/
11+
let selected = "x";
12+
selected = "z"; // should fail
13+
14+
15+
//// [file.js]
16+
// @ts-check
17+
var obj = {
18+
x: 1,
19+
y: 2
20+
};
21+
/**
22+
* @type {keyof typeof obj}
23+
*/
24+
var selected = "x";
25+
selected = "z"; // should fail
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
=== tests/cases/compiler/file.js ===
2+
// @ts-check
3+
const obj = {
4+
>obj : Symbol(obj, Decl(file.js, 1, 5))
5+
6+
x: 1,
7+
>x : Symbol(x, Decl(file.js, 1, 13))
8+
9+
y: 2
10+
>y : Symbol(y, Decl(file.js, 2, 9))
11+
12+
};
13+
14+
/**
15+
* @type {keyof typeof obj}
16+
*/
17+
let selected = "x";
18+
>selected : Symbol(selected, Decl(file.js, 9, 3))
19+
20+
selected = "z"; // should fail
21+
>selected : Symbol(selected, Decl(file.js, 9, 3))
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
=== tests/cases/compiler/file.js ===
2+
// @ts-check
3+
const obj = {
4+
>obj : { x: number; y: number; }
5+
>{ x: 1, y: 2} : { x: number; y: number; }
6+
7+
x: 1,
8+
>x : number
9+
>1 : 1
10+
11+
y: 2
12+
>y : number
13+
>2 : 2
14+
15+
};
16+
17+
/**
18+
* @type {keyof typeof obj}
19+
*/
20+
let selected = "x";
21+
>selected : "x" | "y"
22+
>"x" : "x"
23+
24+
selected = "z"; // should fail
25+
>selected = "z" : "z"
26+
>selected : "x" | "y"
27+
>"z" : "z"
28+

0 commit comments

Comments
 (0)