From 5046882025e3bc8cb122ecef703aebd0b5e79017 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 18 Jan 2022 00:32:37 +1300 Subject: [PATCH] fix(type-utils): intersection types involving readonly arrays are now handled in most cases (#4429) --- packages/type-utils/src/isTypeReadonly.ts | 30 ++++++++++++- .../type-utils/tests/isTypeReadonly.test.ts | 44 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/type-utils/src/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts index 0c5b30d83f1..0ec24438943 100644 --- a/packages/type-utils/src/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -3,10 +3,10 @@ import { isConditionalType, isObjectType, isUnionType, - isUnionOrIntersectionType, unionTypeParts, isPropertyReadonlyInType, isSymbolFlagSet, + isIntersectionType, } from 'tsutils'; import * as ts from 'typescript'; import { getTypeOfPropertyOfType } from './propertyTypes'; @@ -224,6 +224,32 @@ function isTypeReadonlyRecurser( return readonlyness; } + if (isIntersectionType(type)) { + // Special case for handling arrays/tuples (as readonly arrays/tuples always have mutable methods). + if ( + type.types.some(t => checker.isArrayType(t) || checker.isTupleType(t)) + ) { + const allReadonlyParts = type.types.every( + t => + seenTypes.has(t) || + isTypeReadonlyRecurser(checker, t, options, seenTypes) === + Readonlyness.Readonly, + ); + return allReadonlyParts ? Readonlyness.Readonly : Readonlyness.Mutable; + } + + // Normal case. + const isReadonlyObject = isTypeReadonlyObject( + checker, + type, + options, + seenTypes, + ); + if (isReadonlyObject !== Readonlyness.UnknownType) { + return isReadonlyObject; + } + } + if (isConditionalType(type)) { const result = [type.root.node.trueType, type.root.node.falseType] .map(checker.getTypeFromTypeNode) @@ -240,7 +266,7 @@ function isTypeReadonlyRecurser( // all non-object, non-intersection types are readonly. // this should only be primitive types - if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { + if (!isObjectType(type)) { return Readonlyness.Readonly; } diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index 0924bd6122f..f6f2cebd12b 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -165,6 +165,50 @@ describe('isTypeReadonly', () => { }); }); + describe('Intersection', () => { + describe('is readonly', () => { + const runTests = runTestIsReadonly; + + it.each([ + [ + 'type Test = Readonly<{ foo: string; bar: number; }> & Readonly<{ bar: number; }>;', + ], + ])('handles an intersection of 2 fully readonly types', runTests); + + it.each([ + [ + 'type Test = Readonly<{ foo: string; bar: number; }> & { foo: string; };', + ], + ])( + 'handles an intersection of a fully readonly type with a mutable subtype', + runTests, + ); + + // Array - special case. + // Note: Methods are mutable but arrays are treated special; hence no failure. + it.each([ + ['type Test = ReadonlyArray & Readonly<{ foo: string; }>;'], + [ + 'type Test = readonly [string, number] & Readonly<{ foo: string; }>;', + ], + ])('handles an intersections involving a readonly array', runTests); + }); + + describe('is not readonly', () => { + const runTests = runTestIsNotReadonly; + + it.each([ + ['type Test = { foo: string; bar: number; } & { bar: number; };'], + [ + 'type Test = { foo: string; bar: number; } & Readonly<{ bar: number; }>;', + ], + [ + 'type Test = Readonly<{ bar: number; }> & { foo: string; bar: number; };', + ], + ])('handles an intersection of non fully readonly types', runTests); + }); + }); + describe('Conditional Types', () => { describe('is readonly', () => { const runTests = runTestIsReadonly;