Skip to content

⬅️ Back to Table of Contents

📄 isTypeReadonly.ts

📊 Analysis Summary

Metric Count
🔧 Functions 7
📦 Imports 6
📊 Variables & Constants 5
📐 Interfaces 1
🎯 Enums 1

📚 Table of Contents

🛠️ File Location:

📂 packages/type-utils/src/isTypeReadonly.ts

📦 Imports

Name Source
JSONSchema4 @typescript-eslint/utils/json-schema
ESLintUtils @typescript-eslint/utils
TypeOrValueSpecifier ./TypeOrValueSpecifier
getTypeOfPropertyOfType ./propertyTypes
typeMatchesSomeSpecifier ./TypeOrValueSpecifier
typeOrValueSpecifiersSchema ./TypeOrValueSpecifier

Variables & Constants

Name Type Kind Value Exported
readonlynessOptionsSchema JSONSchema4 const `{
additionalProperties: false,
properties: {
allow: typeOrValueSpecifiersSchema,
treatMethodsAsReadonly: {
type: 'boolean',
},
},
type: 'object',
} satisfies JSONSchema4`
readonlynessOptionsDefaults ReadonlynessOptions const `{
allow: [],
treatMethodsAsReadonly: false,
}`
lastDeclaration any const `declarations != null && declarations.length > 0
? declarations[declarations.length - 1]
: undefined`
readonlyness Readonlyness.Mutable | Readonlyness.Readonly const result ? Readonlyness.Readonly : Readonlyness.Mutable
readonlyness Readonlyness.Mutable | Readonlyness.Readonly const result ? Readonlyness.Readonly : Readonlyness.Mutable

Functions

hasSymbol(node: ts.Node): node is { symbol: ts.Symbol } & ts.Node

Code
function hasSymbol(node: ts.Node): node is { symbol: ts.Symbol } & ts.Node {
  return Object.hasOwn(node, 'symbol');
}
  • Parameters:
  • node: ts.Node
  • Return Type: node is { symbol: ts.Symbol } & ts.Node
  • Calls:
  • Object.hasOwn

isTypeReadonlyArrayOrTuple(program: ts.Program, type: ts.Type, options: ReadonlynessOptions, seenTypes: Set<ts.Type>): Readonlyness

Code
function isTypeReadonlyArrayOrTuple(
  program: ts.Program,
  type: ts.Type,
  options: ReadonlynessOptions,
  seenTypes: Set<ts.Type>,
): Readonlyness {
  const checker = program.getTypeChecker();
  function checkTypeArguments(arrayType: ts.TypeReference): Readonlyness {
    const typeArguments = checker.getTypeArguments(arrayType);

    // this shouldn't happen in reality as:
    // - tuples require at least 1 type argument
    // - ReadonlyArray requires at least 1 type argument
    /* istanbul ignore if */ if (typeArguments.length === 0) {
      return Readonlyness.Readonly;
    }

    // validate the element types are also readonly
    if (
      typeArguments.some(
        typeArg =>
          isTypeReadonlyRecurser(program, typeArg, options, seenTypes) ===
          Readonlyness.Mutable,
      )
    ) {
      return Readonlyness.Mutable;
    }
    return Readonlyness.Readonly;
  }

  if (checker.isArrayType(type)) {
    const symbol = ESLintUtils.nullThrows(
      type.getSymbol(),
      ESLintUtils.NullThrowsReasons.MissingToken('symbol', 'array type'),
    );
    const escapedName = symbol.getEscapedName();
    // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
    if (escapedName === 'Array') {
      return Readonlyness.Mutable;
    }

    return checkTypeArguments(type);
  }

  if (checker.isTupleType(type)) {
    if (!type.target.readonly) {
      return Readonlyness.Mutable;
    }

    return checkTypeArguments(type);
  }

  return Readonlyness.UnknownType;
}
  • Parameters:
  • program: ts.Program
  • type: ts.Type
  • options: ReadonlynessOptions
  • seenTypes: Set<ts.Type>
  • Return Type: Readonlyness
  • Calls:
  • program.getTypeChecker
  • checker.getTypeArguments
  • typeArguments.some
  • isTypeReadonlyRecurser
  • checker.isArrayType
  • ESLintUtils.nullThrows
  • type.getSymbol
  • ESLintUtils.NullThrowsReasons.MissingToken
  • symbol.getEscapedName
  • checkTypeArguments
  • checker.isTupleType
  • Internal Comments:
    // this shouldn't happen in reality as:
    // - tuples require at least 1 type argument
    // - ReadonlyArray requires at least 1 type argument
    /* istanbul ignore if */
    // validate the element types are also readonly
    // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
    

checkTypeArguments(arrayType: ts.TypeReference): Readonlyness

Code
function checkTypeArguments(arrayType: ts.TypeReference): Readonlyness {
    const typeArguments = checker.getTypeArguments(arrayType);

    // this shouldn't happen in reality as:
    // - tuples require at least 1 type argument
    // - ReadonlyArray requires at least 1 type argument
    /* istanbul ignore if */ if (typeArguments.length === 0) {
      return Readonlyness.Readonly;
    }

    // validate the element types are also readonly
    if (
      typeArguments.some(
        typeArg =>
          isTypeReadonlyRecurser(program, typeArg, options, seenTypes) ===
          Readonlyness.Mutable,
      )
    ) {
      return Readonlyness.Mutable;
    }
    return Readonlyness.Readonly;
  }
  • Parameters:
  • arrayType: ts.TypeReference
  • Return Type: Readonlyness
  • Calls:
  • checker.getTypeArguments
  • typeArguments.some
  • isTypeReadonlyRecurser
  • Internal Comments:
    // this shouldn't happen in reality as:
    // - tuples require at least 1 type argument
    // - ReadonlyArray requires at least 1 type argument
    /* istanbul ignore if */
    // validate the element types are also readonly
    

isTypeReadonlyObject(program: ts.Program, type: ts.Type, options: ReadonlynessOptions, seenTypes: Set<ts.Type>): Readonlyness

Code
function isTypeReadonlyObject(
  program: ts.Program,
  type: ts.Type,
  options: ReadonlynessOptions,
  seenTypes: Set<ts.Type>,
): Readonlyness {
  const checker = program.getTypeChecker();
  function checkIndexSignature(kind: ts.IndexKind): Readonlyness {
    const indexInfo = checker.getIndexInfoOfType(type, kind);
    if (indexInfo) {
      if (!indexInfo.isReadonly) {
        return Readonlyness.Mutable;
      }

      if (indexInfo.type === type || seenTypes.has(indexInfo.type)) {
        return Readonlyness.Readonly;
      }

      return isTypeReadonlyRecurser(
        program,
        indexInfo.type,
        options,
        seenTypes,
      );
    }

    return Readonlyness.UnknownType;
  }

  const properties = type.getProperties();
  if (properties.length) {
    // ensure the properties are marked as readonly
    for (const property of properties) {
      if (options.treatMethodsAsReadonly) {
        if (
          property.valueDeclaration != null &&
          hasSymbol(property.valueDeclaration) &&
          tsutils.isSymbolFlagSet(
            property.valueDeclaration.symbol,
            ts.SymbolFlags.Method,
          )
        ) {
          continue;
        }

        const declarations = property.getDeclarations();
        const lastDeclaration =
          declarations != null && declarations.length > 0
            ? declarations[declarations.length - 1]
            : undefined;
        if (
          lastDeclaration != null &&
          hasSymbol(lastDeclaration) &&
          tsutils.isSymbolFlagSet(lastDeclaration.symbol, ts.SymbolFlags.Method)
        ) {
          continue;
        }
      }

      if (
        tsutils.isPropertyReadonlyInType(
          type,
          property.getEscapedName(),
          checker,
        )
      ) {
        continue;
      }

      const name = ts.getNameOfDeclaration(property.valueDeclaration);
      if (name && ts.isPrivateIdentifier(name)) {
        continue;
      }

      return Readonlyness.Mutable;
    }

    // all properties were readonly
    // now ensure that all of the values are readonly also.

    // do this after checking property readonly-ness as a perf optimization,
    // as we might be able to bail out early due to a mutable property before
    // doing this deep, potentially expensive check.
    for (const property of properties) {
      const propertyType = ESLintUtils.nullThrows(
        getTypeOfPropertyOfType(checker, type, property),
        ESLintUtils.NullThrowsReasons.MissingToken(
          `property "${property.name}"`,
          'type',
        ),
      );

      // handle recursive types.
      // we only need this simple check, because a mutable recursive type will break via the above prop readonly check
      if (seenTypes.has(propertyType)) {
        continue;
      }

      if (
        isTypeReadonlyRecurser(program, propertyType, options, seenTypes) ===
        Readonlyness.Mutable
      ) {
        return Readonlyness.Mutable;
      }
    }
  }

  const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String);
  if (isStringIndexSigReadonly === Readonlyness.Mutable) {
    return isStringIndexSigReadonly;
  }

  const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number);
  if (isNumberIndexSigReadonly === Readonlyness.Mutable) {
    return isNumberIndexSigReadonly;
  }

  return Readonlyness.Readonly;
}
  • Parameters:
  • program: ts.Program
  • type: ts.Type
  • options: ReadonlynessOptions
  • seenTypes: Set<ts.Type>
  • Return Type: Readonlyness
  • Calls:
  • program.getTypeChecker
  • checker.getIndexInfoOfType
  • seenTypes.has
  • isTypeReadonlyRecurser
  • type.getProperties
  • hasSymbol
  • tsutils.isSymbolFlagSet
  • property.getDeclarations
  • tsutils.isPropertyReadonlyInType
  • property.getEscapedName
  • ts.getNameOfDeclaration
  • ts.isPrivateIdentifier
  • ESLintUtils.nullThrows
  • getTypeOfPropertyOfType (from ./propertyTypes)
  • ESLintUtils.NullThrowsReasons.MissingToken
  • checkIndexSignature
  • Internal Comments:
    // ensure the properties are marked as readonly
    // all properties were readonly
    // now ensure that all of the values are readonly also.
    // do this after checking property readonly-ness as a perf optimization,
    // as we might be able to bail out early due to a mutable property before
    // doing this deep, potentially expensive check.
    // handle recursive types.
    // we only need this simple check, because a mutable recursive type will break via the above prop readonly check
    

checkIndexSignature(kind: ts.IndexKind): Readonlyness

Code
function checkIndexSignature(kind: ts.IndexKind): Readonlyness {
    const indexInfo = checker.getIndexInfoOfType(type, kind);
    if (indexInfo) {
      if (!indexInfo.isReadonly) {
        return Readonlyness.Mutable;
      }

      if (indexInfo.type === type || seenTypes.has(indexInfo.type)) {
        return Readonlyness.Readonly;
      }

      return isTypeReadonlyRecurser(
        program,
        indexInfo.type,
        options,
        seenTypes,
      );
    }

    return Readonlyness.UnknownType;
  }
  • Parameters:
  • kind: ts.IndexKind
  • Return Type: Readonlyness
  • Calls:
  • checker.getIndexInfoOfType
  • seenTypes.has
  • isTypeReadonlyRecurser

isTypeReadonlyRecurser(program: ts.Program, type: ts.Type, options: ReadonlynessOptions, seenTypes: Set<ts.Type>): Readonlyness.Mutable | Readonlyness.Readonly

Code
function isTypeReadonlyRecurser(
  program: ts.Program,
  type: ts.Type,
  options: ReadonlynessOptions,
  seenTypes: Set<ts.Type>,
): Readonlyness.Mutable | Readonlyness.Readonly {
  const checker = program.getTypeChecker();
  seenTypes.add(type);

  if (typeMatchesSomeSpecifier(type, options.allow, program)) {
    return Readonlyness.Readonly;
  }

  if (tsutils.isUnionType(type)) {
    // all types in the union must be readonly
    const result = tsutils
      .unionConstituents(type)
      .every(
        t =>
          seenTypes.has(t) ||
          isTypeReadonlyRecurser(program, t, options, seenTypes) ===
            Readonlyness.Readonly,
      );
    const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable;
    return readonlyness;
  }

  if (tsutils.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(program, t, options, seenTypes) ===
            Readonlyness.Readonly,
      );
      return allReadonlyParts ? Readonlyness.Readonly : Readonlyness.Mutable;
    }

    // Normal case.
    const isReadonlyObject = isTypeReadonlyObject(
      program,
      type,
      options,
      seenTypes,
    );
    if (isReadonlyObject !== Readonlyness.UnknownType) {
      return isReadonlyObject;
    }
  }

  if (tsutils.isConditionalType(type)) {
    const result = [type.root.node.trueType, type.root.node.falseType]
      .map(checker.getTypeFromTypeNode)
      .every(
        t =>
          seenTypes.has(t) ||
          isTypeReadonlyRecurser(program, t, options, seenTypes) ===
            Readonlyness.Readonly,
      );

    const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable;
    return readonlyness;
  }

  // all non-object, non-intersection types are readonly.
  // this should only be primitive types
  if (!tsutils.isObjectType(type)) {
    return Readonlyness.Readonly;
  }

  // pure function types are readonly
  if (
    type.getCallSignatures().length > 0 &&
    type.getProperties().length === 0
  ) {
    return Readonlyness.Readonly;
  }

  const isReadonlyArray = isTypeReadonlyArrayOrTuple(
    program,
    type,
    options,
    seenTypes,
  );
  if (isReadonlyArray !== Readonlyness.UnknownType) {
    return isReadonlyArray;
  }

  const isReadonlyObject = isTypeReadonlyObject(
    program,
    type,
    options,
    seenTypes,
  );
  /* istanbul ignore else */ if (
    isReadonlyObject !== Readonlyness.UnknownType
  ) {
    return isReadonlyObject;
  }

  throw new Error('Unhandled type');
}
  • Parameters:
  • program: ts.Program
  • type: ts.Type
  • options: ReadonlynessOptions
  • seenTypes: Set<ts.Type>
  • Return Type: Readonlyness.Mutable | Readonlyness.Readonly
  • Calls:
  • program.getTypeChecker
  • seenTypes.add
  • typeMatchesSomeSpecifier (from ./TypeOrValueSpecifier)
  • tsutils.isUnionType
  • tsutils .unionConstituents(type) .every
  • seenTypes.has
  • isTypeReadonlyRecurser
  • tsutils.isIntersectionType
  • type.types.some
  • checker.isArrayType
  • checker.isTupleType
  • type.types.every
  • isTypeReadonlyObject
  • tsutils.isConditionalType
  • [type.root.node.trueType, type.root.node.falseType] .map(checker.getTypeFromTypeNode) .every
  • tsutils.isObjectType
  • type.getCallSignatures
  • type.getProperties
  • isTypeReadonlyArrayOrTuple
  • Internal Comments:
    // all types in the union must be readonly (x2)
    // Special case for handling arrays/tuples (as readonly arrays/tuples always have mutable methods).
    // Normal case. (x2)
    // all non-object, non-intersection types are readonly.
    // this should only be primitive types
    // pure function types are readonly
    /* istanbul ignore else */
    

isTypeReadonly(program: ts.Program, type: ts.Type, options: ReadonlynessOptions): boolean

Code
export function isTypeReadonly(
  program: ts.Program,
  type: ts.Type,
  options: ReadonlynessOptions = readonlynessOptionsDefaults,
): boolean {
  return (
    isTypeReadonlyRecurser(program, type, options, new Set()) ===
    Readonlyness.Readonly
  );
}
  • JSDoc:

    /**
     * Checks if the given type is readonly
     */
    

  • Parameters:

  • program: ts.Program
  • type: ts.Type
  • options: ReadonlynessOptions
  • Return Type: boolean
  • Calls:
  • isTypeReadonlyRecurser

Interfaces

ReadonlynessOptions

Interface Code
export interface ReadonlynessOptions {
  readonly allow?: TypeOrValueSpecifier[];
  readonly treatMethodsAsReadonly?: boolean;
}

Properties

Name Type Optional Description
allow TypeOrValueSpecifier[]
treatMethodsAsReadonly boolean

Enums

const enum Readonlyness

Enum Code
const enum Readonlyness {
  /** the type cannot be handled by the function */
  UnknownType = 1,
  /** the type is mutable */
  Mutable = 2,
  /** the type is readonly */
  Readonly = 3,
}

Members

Name Value Description
UnknownType 1 / the type cannot be handled by the function */
Mutable 2 / the type is mutable */
Readonly 3 / the type is readonly */