Skip to content

Commit ef3147c

Browse files
RebeccaStevensJosh Goldberg
and
Josh Goldberg
authored
fix(type-utils): check IndexSignature internals when checking isTypeReadonly (#4417)
* fix(type-utils): make isTypeReadonly's options param optional fix #4410 * test(type-utils): add basic tests for isTypeReadonly * test(type-utils): add test for IndexSignature internals * fix(type-utils): check IndexSignature internals when checking isTypeReadonly fix #3714 * perf(type-utils): don't test IndexSignature key for readonlyness as it will always be readonly Co-authored-by: Josh Goldberg <[email protected]>
1 parent 3061ea9 commit ef3147c

File tree

2 files changed

+68
-16
lines changed

2 files changed

+68
-16
lines changed

Diff for: packages/type-utils/src/isTypeReadonly.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,16 @@ function isTypeReadonlyObject(
107107
function checkIndexSignature(kind: ts.IndexKind): Readonlyness {
108108
const indexInfo = checker.getIndexInfoOfType(type, kind);
109109
if (indexInfo) {
110-
return indexInfo.isReadonly
111-
? Readonlyness.Readonly
112-
: Readonlyness.Mutable;
110+
if (!indexInfo.isReadonly) {
111+
return Readonlyness.Mutable;
112+
}
113+
114+
return isTypeReadonlyRecurser(
115+
checker,
116+
indexInfo.type,
117+
options,
118+
seenTypes,
119+
);
113120
}
114121

115122
return Readonlyness.UnknownType;
@@ -119,20 +126,37 @@ function isTypeReadonlyObject(
119126
if (properties.length) {
120127
// ensure the properties are marked as readonly
121128
for (const property of properties) {
122-
if (
123-
!(
124-
isPropertyReadonlyInType(type, property.getEscapedName(), checker) ||
125-
(options.treatMethodsAsReadonly &&
126-
property.valueDeclaration !== undefined &&
127-
hasSymbol(property.valueDeclaration) &&
128-
isSymbolFlagSet(
129-
property.valueDeclaration.symbol,
130-
ts.SymbolFlags.Method,
131-
))
132-
)
133-
) {
134-
return Readonlyness.Mutable;
129+
if (options.treatMethodsAsReadonly) {
130+
if (
131+
property.valueDeclaration !== undefined &&
132+
hasSymbol(property.valueDeclaration) &&
133+
isSymbolFlagSet(
134+
property.valueDeclaration.symbol,
135+
ts.SymbolFlags.Method,
136+
)
137+
) {
138+
continue;
139+
}
140+
141+
const declarations = property.getDeclarations();
142+
const lastDeclaration =
143+
declarations !== undefined && declarations.length > 0
144+
? declarations[declarations.length - 1]
145+
: undefined;
146+
if (
147+
lastDeclaration !== undefined &&
148+
hasSymbol(lastDeclaration) &&
149+
isSymbolFlagSet(lastDeclaration.symbol, ts.SymbolFlags.Method)
150+
) {
151+
continue;
152+
}
153+
}
154+
155+
if (isPropertyReadonlyInType(type, property.getEscapedName(), checker)) {
156+
continue;
135157
}
158+
159+
return Readonlyness.Mutable;
136160
}
137161

138162
// all properties were readonly

Diff for: packages/type-utils/tests/isTypeReadonly.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,34 @@ describe('isTypeReadonly', () => {
109109
])('handles non fully readonly sets and maps', runTests);
110110
});
111111
});
112+
113+
describe('IndexSignature', () => {
114+
describe('is readonly', () => {
115+
const runTests = runTestIsReadonly;
116+
117+
it.each([
118+
['type Test = { readonly [key: string]: string };'],
119+
[
120+
'type Test = { readonly [key: string]: { readonly foo: readonly string[]; }; };',
121+
],
122+
])(
123+
'handles readonly PropertySignature inside a readonly IndexSignature',
124+
runTests,
125+
);
126+
});
127+
128+
describe('is not readonly', () => {
129+
const runTests = runTestIsNotReadonly;
130+
131+
it.each([
132+
['type Test = { [key: string]: string };'],
133+
['type Test = { readonly [key: string]: { foo: string[]; }; };'],
134+
])(
135+
'handles mutable PropertySignature inside a readonly IndexSignature',
136+
runTests,
137+
);
138+
});
139+
});
112140
});
113141

114142
describe('treatMethodsAsReadonly', () => {

0 commit comments

Comments
 (0)