diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cd33d83ff49..1c78f3315b8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19732,6 +19732,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) { diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index f6975716075..dc2918572d0 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -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(); // Set implemented as Map + private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; public static fromContext(context: TextChangesContext): ChangeTracker { diff --git a/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.errors.txt b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.errors.txt new file mode 100644 index 00000000000..dab2892c5fd --- /dev/null +++ b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.errors.txt @@ -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; + } \ No newline at end of file diff --git a/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.js b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.js new file mode 100644 index 00000000000..2ef516de0ca --- /dev/null +++ b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.js @@ -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; +} diff --git a/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.symbols b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.symbols new file mode 100644 index 00000000000..9a19925057e --- /dev/null +++ b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.symbols @@ -0,0 +1,162 @@ +=== tests/cases/compiler/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' }; +>two : Symbol(two, Decl(one.ts, 2, 5)) +>a : Symbol(a, Decl(one.ts, 2, 12)) +>a : Symbol(a, Decl(one.ts, 2, 28)) + +const one: { readonly a: string } = { a: 'one' }; +>one : Symbol(one, Decl(one.ts, 3, 5)) +>a : Symbol(a, Decl(one.ts, 3, 12)) +>a : Symbol(a, Decl(one.ts, 3, 37)) + +function doSomething(condition: boolean) { +>doSomething : Symbol(doSomething, Decl(one.ts, 3, 49)) +>condition : Symbol(condition, Decl(one.ts, 5, 21)) + + // 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 : Symbol(three, Decl(one.ts, 8, 9)) +>condition : Symbol(condition, Decl(one.ts, 5, 21)) +>one : Symbol(one, Decl(one.ts, 3, 5)) +>two : Symbol(two, Decl(one.ts, 2, 5)) + + three.a = 'foo'; +>three.a : Symbol(a, Decl(one.ts, 3, 12)) +>three : Symbol(three, Decl(one.ts, 8, 9)) +>a : Symbol(a, Decl(one.ts, 3, 12)) + + // 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'; +>three.a : Symbol(a, Decl(one.ts, 3, 12)) +>three : Symbol(three, Decl(one.ts, 8, 9)) +>a : Symbol(a, Decl(one.ts, 3, 12)) + + return three; +>three : Symbol(three, Decl(one.ts, 8, 9)) +} +=== tests/cases/compiler/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' }; +>two : Symbol(two, Decl(two.ts, 2, 5)) +>a : Symbol(a, Decl(two.ts, 2, 12)) +>a : Symbol(a, Decl(two.ts, 2, 28)) + +const one: { readonly a: string } = { a: 'one' }; +>one : Symbol(one, Decl(two.ts, 3, 5)) +>a : Symbol(a, Decl(two.ts, 3, 12)) +>a : Symbol(a, Decl(two.ts, 3, 37)) + +function doSomething(condition: boolean) { +>doSomething : Symbol(doSomething, Decl(two.ts, 3, 49)) +>condition : Symbol(condition, Decl(two.ts, 5, 21)) + + // 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 : Symbol(three, Decl(two.ts, 8, 9)) +>condition : Symbol(condition, Decl(two.ts, 5, 21)) +>two : Symbol(two, Decl(two.ts, 2, 5)) +>one : Symbol(one, Decl(two.ts, 3, 5)) + + three.a = 'foo'; +>three.a : Symbol(a, Decl(two.ts, 3, 12)) +>three : Symbol(three, Decl(two.ts, 8, 9)) +>a : Symbol(a, Decl(two.ts, 3, 12)) + + // 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'; +>three.a : Symbol(a, Decl(two.ts, 3, 12)) +>three : Symbol(three, Decl(two.ts, 8, 9)) +>a : Symbol(a, Decl(two.ts, 3, 12)) + + return three; +>three : Symbol(three, Decl(two.ts, 8, 9)) +} + +=== tests/cases/compiler/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' }; +>one : Symbol(one, Decl(three.ts, 2, 5)) +>a : Symbol(a, Decl(three.ts, 2, 12)) +>a : Symbol(a, Decl(three.ts, 2, 37)) + +const two: { a: string } = { a: 'two' }; +>two : Symbol(two, Decl(three.ts, 3, 5)) +>a : Symbol(a, Decl(three.ts, 3, 12)) +>a : Symbol(a, Decl(three.ts, 3, 28)) + +function doSomething(condition: boolean) { +>doSomething : Symbol(doSomething, Decl(three.ts, 3, 40)) +>condition : Symbol(condition, Decl(three.ts, 5, 21)) + + // 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 : Symbol(three, Decl(three.ts, 8, 9)) +>condition : Symbol(condition, Decl(three.ts, 5, 21)) +>one : Symbol(one, Decl(three.ts, 2, 5)) +>two : Symbol(two, Decl(three.ts, 3, 5)) + + three.a = 'foo'; +>three.a : Symbol(a, Decl(three.ts, 2, 12)) +>three : Symbol(three, Decl(three.ts, 8, 9)) +>a : Symbol(a, Decl(three.ts, 2, 12)) + + // 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'; +>three.a : Symbol(a, Decl(three.ts, 2, 12)) +>three : Symbol(three, Decl(three.ts, 8, 9)) +>a : Symbol(a, Decl(three.ts, 2, 12)) + + return three; +>three : Symbol(three, Decl(three.ts, 8, 9)) +} + +=== tests/cases/compiler/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' }; +>one : Symbol(one, Decl(four.ts, 2, 5)) +>a : Symbol(a, Decl(four.ts, 2, 12)) +>a : Symbol(a, Decl(four.ts, 2, 37)) + +const two: { a: string } = { a: 'two' }; +>two : Symbol(two, Decl(four.ts, 3, 5)) +>a : Symbol(a, Decl(four.ts, 3, 12)) +>a : Symbol(a, Decl(four.ts, 3, 28)) + +function doSomething(condition: boolean) { +>doSomething : Symbol(doSomething, Decl(four.ts, 3, 40)) +>condition : Symbol(condition, Decl(four.ts, 5, 21)) + + // 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 : Symbol(three, Decl(four.ts, 8, 9)) +>condition : Symbol(condition, Decl(four.ts, 5, 21)) +>two : Symbol(two, Decl(four.ts, 3, 5)) +>one : Symbol(one, Decl(four.ts, 2, 5)) + + three.a = 'foo'; +>three.a : Symbol(a, Decl(four.ts, 2, 12)) +>three : Symbol(three, Decl(four.ts, 8, 9)) +>a : Symbol(a, Decl(four.ts, 2, 12)) + + // 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'; +>three.a : Symbol(a, Decl(four.ts, 2, 12)) +>three : Symbol(three, Decl(four.ts, 8, 9)) +>a : Symbol(a, Decl(four.ts, 2, 12)) + + return three; +>three : Symbol(three, Decl(four.ts, 8, 9)) +} diff --git a/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.types b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.types new file mode 100644 index 00000000000..30e4b0d32c7 --- /dev/null +++ b/tests/baselines/reference/readonlyPropertySubtypeRelationDirected.types @@ -0,0 +1,202 @@ +=== tests/cases/compiler/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' }; +>two : { a: string; } +>a : string +>{ a: 'two' } : { a: string; } +>a : string +>'two' : "two" + +const one: { readonly a: string } = { a: 'one' }; +>one : { readonly a: string; } +>a : string +>{ a: 'one' } : { a: string; } +>a : string +>'one' : "one" + +function doSomething(condition: boolean) { +>doSomething : (condition: boolean) => { readonly a: string; } +>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 : { readonly a: string; } +>(condition) ? one : two : { readonly a: string; } +>(condition) : boolean +>condition : boolean +>one : { readonly a: string; } +>two : { a: string; } + + three.a = 'foo'; +>three.a = 'foo' : "foo" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo' : "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'; +>three.a = 'foo2' : "foo2" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo2' : "foo2" + + return three; +>three : { readonly a: string; } +} +=== tests/cases/compiler/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' }; +>two : { a: string; } +>a : string +>{ a: 'two' } : { a: string; } +>a : string +>'two' : "two" + +const one: { readonly a: string } = { a: 'one' }; +>one : { readonly a: string; } +>a : string +>{ a: 'one' } : { a: string; } +>a : string +>'one' : "one" + +function doSomething(condition: boolean) { +>doSomething : (condition: boolean) => { readonly a: string; } +>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 : { readonly a: string; } +>(condition) ? two : one : { readonly a: string; } +>(condition) : boolean +>condition : boolean +>two : { a: string; } +>one : { readonly a: string; } + + three.a = 'foo'; +>three.a = 'foo' : "foo" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo' : "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'; +>three.a = 'foo2' : "foo2" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo2' : "foo2" + + return three; +>three : { readonly a: string; } +} + +=== tests/cases/compiler/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' }; +>one : { readonly a: string; } +>a : string +>{ a: 'one' } : { a: string; } +>a : string +>'one' : "one" + +const two: { a: string } = { a: 'two' }; +>two : { a: string; } +>a : string +>{ a: 'two' } : { a: string; } +>a : string +>'two' : "two" + +function doSomething(condition: boolean) { +>doSomething : (condition: boolean) => { readonly a: string; } +>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 : { readonly a: string; } +>(condition) ? one : two : { readonly a: string; } +>(condition) : boolean +>condition : boolean +>one : { readonly a: string; } +>two : { a: string; } + + three.a = 'foo'; +>three.a = 'foo' : "foo" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo' : "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'; +>three.a = 'foo2' : "foo2" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo2' : "foo2" + + return three; +>three : { readonly a: string; } +} + +=== tests/cases/compiler/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' }; +>one : { readonly a: string; } +>a : string +>{ a: 'one' } : { a: string; } +>a : string +>'one' : "one" + +const two: { a: string } = { a: 'two' }; +>two : { a: string; } +>a : string +>{ a: 'two' } : { a: string; } +>a : string +>'two' : "two" + +function doSomething(condition: boolean) { +>doSomething : (condition: boolean) => { readonly a: string; } +>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 : { readonly a: string; } +>(condition) ? two : one : { readonly a: string; } +>(condition) : boolean +>condition : boolean +>two : { a: string; } +>one : { readonly a: string; } + + three.a = 'foo'; +>three.a = 'foo' : "foo" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo' : "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'; +>three.a = 'foo2' : "foo2" +>three.a : any +>three : { readonly a: string; } +>a : any +>'foo2' : "foo2" + + return three; +>three : { readonly a: string; } +} diff --git a/tests/cases/compiler/readonlyPropertySubtypeRelationDirected.ts b/tests/cases/compiler/readonlyPropertySubtypeRelationDirected.ts new file mode 100644 index 00000000000..aadd4bec5a0 --- /dev/null +++ b/tests/cases/compiler/readonlyPropertySubtypeRelationDirected.ts @@ -0,0 +1,79 @@ +// @strict: true +// @filename: 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; +} +// @filename: 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; +} + +// @filename: 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; +} + +// @filename: 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; +} \ No newline at end of file