Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19725,6 +19725,18 @@ namespace ts {
}
return Ternary.False;
}

// Ensure {readonly a: whatever} is not a subtype of {a: whatever},
// while {a: whatever} is a subtype of {readonly a: whatever}.
// This ensures the subtype relationship is ordered, and preventing declaration order
// from deciding which type "wins" in union subtype reduction.
// They're still assignable to one another, since `readonly` doesn't affect assignability.
if (
(relation === subtypeRelation || relation === strictSubtypeRelation) &&
!!(sourcePropFlags & ModifierFlags.Readonly) && !(targetPropFlags & ModifierFlags.Readonly)
) {
return Ternary.False;
}
// If the target comes from a partial union prop, allow `undefined` in the target type
const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, intersectionState);
if (!related) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ namespace ts.textChanges {
export class ChangeTracker {
private readonly changes: Change[] = [];
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = [];
private readonly classesWithNodesInsertedAtStart = new Map<number, { readonly node: ClassDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
private readonly classesWithNodesInsertedAtStart = new Map<number, { readonly node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];

public static fromContext(context: TextChangesContext): ChangeTracker {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
tests/cases/compiler/four.ts(11,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/four.ts(15,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/one.ts(11,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/one.ts(15,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/three.ts(11,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/three.ts(15,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/two.ts(11,11): error TS2540: Cannot assign to 'a' because it is a read-only property.
tests/cases/compiler/two.ts(15,11): error TS2540: Cannot assign to 'a' because it is a read-only property.


==== tests/cases/compiler/one.ts (2 errors) ====
export {};
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
const two: { a: string } = { a: 'two' };
const one: { readonly a: string } = { a: 'one' };

function doSomething(condition: boolean) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
const three = (condition) ? one : two;

three.a = 'foo';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

return three;
}
==== tests/cases/compiler/two.ts (2 errors) ====
export {};
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
const two: { a: string } = { a: 'two' };
const one: { readonly a: string } = { a: 'one' };

function doSomething(condition: boolean) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
const three = (condition) ? two : one;

three.a = 'foo';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

return three;
}

==== tests/cases/compiler/three.ts (2 errors) ====
export {};
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
const one: { readonly a: string } = { a: 'one' };
const two: { a: string } = { a: 'two' };

function doSomething(condition: boolean) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
const three = (condition) ? one : two;

three.a = 'foo';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

return three;
}

==== tests/cases/compiler/four.ts (2 errors) ====
export {};
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
const one: { readonly a: string } = { a: 'one' };
const two: { a: string } = { a: 'two' };

function doSomething(condition: boolean) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
const three = (condition) ? two : one;

three.a = 'foo';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
~
!!! error TS2540: Cannot assign to 'a' because it is a read-only property.

return three;
}
145 changes: 145 additions & 0 deletions tests/baselines/reference/readonlyPropertySubtypeRelationDirected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//// [tests/cases/compiler/readonlyPropertySubtypeRelationDirected.ts] ////

//// [one.ts]
export {};
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
const two: { a: string } = { a: 'two' };
const one: { readonly a: string } = { a: 'one' };

function doSomething(condition: boolean) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
const three = (condition) ? one : two;

three.a = 'foo';

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';

return three;
}
//// [two.ts]
export {};
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
const two: { a: string } = { a: 'two' };
const one: { readonly a: string } = { a: 'one' };

function doSomething(condition: boolean) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
const three = (condition) ? two : one;

three.a = 'foo';

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';

return three;
}

//// [three.ts]
export {};
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
const one: { readonly a: string } = { a: 'one' };
const two: { a: string } = { a: 'two' };

function doSomething(condition: boolean) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
const three = (condition) ? one : two;

three.a = 'foo';

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';

return three;
}

//// [four.ts]
export {};
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
const one: { readonly a: string } = { a: 'one' };
const two: { a: string } = { a: 'two' };

function doSomething(condition: boolean) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
const three = (condition) ? two : one;

three.a = 'foo';

// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';

return three;
}

//// [one.js]
"use strict";
exports.__esModule = true;
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
var two = { a: 'two' };
var one = { a: 'one' };
function doSomething(condition) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
var three = (condition) ? one : two;
three.a = 'foo';
// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
return three;
}
//// [two.js]
"use strict";
exports.__esModule = true;
// When the non-readonly type is declared first, the unioned type of `three` in `doSomething` is never treated as readonly
var two = { a: 'two' };
var one = { a: 'one' };
function doSomething(condition) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
var three = (condition) ? two : one;
three.a = 'foo';
// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
return three;
}
//// [three.js]
"use strict";
exports.__esModule = true;
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
var one = { a: 'one' };
var two = { a: 'two' };
function doSomething(condition) {
// when `one` comes first in the conditional check, the return type of `doSomething` is inferred as `a` is readonly, but `a` is
// only treated as readonly (i.e. it will produce a diagnostic if you try to assign to it) based on the order of declarations of `one` and `two` above
var three = (condition) ? one : two;
three.a = 'foo';
// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
return three;
}
//// [four.js]
"use strict";
exports.__esModule = true;
// When the readonly type is declared first, the unioned type of `three` in `doSomething` is always treated as readonly by the compiler
var one = { a: 'one' };
var two = { a: 'two' };
function doSomething(condition) {
// when `two` comes first in the conditional check, the return type of `doSomething` is inferred as not readonly but produces the same diagnostics as above
// based on the declaration order of `one` and `two`
var three = (condition) ? two : one;
three.a = 'foo';
// the inferred (displayed?) type of `a` also depends on the order of the condition above. When `one` comes first, the displayed type is `any`
// when `two` comes first, the displayed type is `string`, but the diagnostic will always correctly find that it's string
three.a = 'foo2';
return three;
}
Loading