Skip to content

Commit

Permalink
fix(type-utils): intersection types involving readonly arrays are now…
Browse files Browse the repository at this point in the history
… handled in most cases (#4429)
  • Loading branch information
RebeccaStevens committed Jan 17, 2022
1 parent 39a6806 commit 5046882
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 2 deletions.
30 changes: 28 additions & 2 deletions packages/type-utils/src/isTypeReadonly.ts
Expand Up @@ -3,10 +3,10 @@ import {
isConditionalType,
isObjectType,
isUnionType,
isUnionOrIntersectionType,
unionTypeParts,
isPropertyReadonlyInType,
isSymbolFlagSet,
isIntersectionType,
} from 'tsutils';
import * as ts from 'typescript';
import { getTypeOfPropertyOfType } from './propertyTypes';
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}

Expand Down
44 changes: 44 additions & 0 deletions packages/type-utils/tests/isTypeReadonly.test.ts
Expand Up @@ -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<string> & 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;
Expand Down

0 comments on commit 5046882

Please sign in to comment.