TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

New `--enforceReadonly` compiler option to enforce read-only semantics in type relations

Open ahejlsberg opened this issue 1 year ago • 45 comments

This PR introduces a new --enforceReadonly compiler option to enforce read-only semantics in type relations. Currently, the readonly modifier prohibits assignments to properties so marked, but it still considers a readonly property to be a match for a mutable property in subtyping and assignability type relationships. With this PR, readonly properties are required to remain readonly across type relationships:

type Box<T> = { value: T };
type ImmutableBox<T> = { readonly value: T };

const immutable: ImmutableBox<string> = { value: "hello" };
const mutable: Box<string> = immutable;  // Error with --enforceReadonly
mutable.value = "ouch!";

When compiled with --enforceReadonly the assignment of immutable to mutable becomes an error because the value property is readonly in the source type but not in the target type.

The --enforceReadonly option also validates that derived classes and interfaces don't violate inherited readonly constraints. For example, an error is reported in the example below because it isn't possible for a derived class to remove mutability from an inherited property (a getter-only declaration is effectively readonly, yet assignments are allowed when treating an instance as the base type).

interface Base {
    x: number;
}

interface Derived extends Base {  // Error with --enforceReadonly
    get x(): number;
}

In type relationships involving generic mapped types, --enforceReadonly ensures that properties of the target type are not more mutable than properties of the source type:

type Mutable<T> = { -readonly [P in keyof T]: T[P] };

function foo<T>(mt: Mutable<T>, tt: T, rt: Readonly<T>) {
    mt = tt;  // Error with --enforceReadonly
    mt = rt;  // Error with --enforceReadonly
    tt = mt;
    tt = rt;  // Error with --enforceReadonly
    rt = mt;
    rt = tt;
}

The --enforceReadonly option slightly modifies the effect of as const assertions and const type parameters to mean "as const as possible" without violating constraints. For example, the following compiles successfully with --enforceReadonly:

const obj: { a: string, b: number } = { a: "hello", b: 42 } as const;

whereas the following does not:

const c = { a: "hello", b: 42 } as const;  // { readonly a: "hello", readonly b: 42 }
const obj: { a: string, b: number } = c;  // Error

Some examples using const type parameters:

declare function f10<T>(obj: T): T;
declare function f11<const T>(obj: T): T;
declare function f12<const T extends { a: string, b: number }>(obj: T): T;
declare function f13<const T extends { a: string, readonly b: number }>(obj: T): T;
declare function f14<const T extends Record<string, unknown>>(obj: T): T;
declare function f15<const T extends Readonly<Record<string, unknown>>>(obj: T): T;

f10({ a: "hello", b: 42 });  // { a: string; b: number; }
f11({ a: "hello", b: 42 });  // { readonly a: "hello"; readonly b: 42; }
f12({ a: "hello", b: 42 });  // { a: "hello"; b: 42; }
f13({ a: "hello", b: 42 });  // { a: "hello"; readonly b: 42; }
f14({ a: "hello", b: 42 });  // { a: "hello"; b: 42; }
f15({ a: "hello", b: 42 });  // { readonly a: "hello"; readonly b: 42; }

Stricter enforcement of readonly has been debated ever since the modifier was introduced eight years ago in #6532. Our rationale for the current design is outlined here. Given the huge body of existing type definitions that were written without deeper consideration of read-only vs. read-write semantics, it would be a significant breaking change to strictly enforce readonly semantics across type relationships. For this reason, the --enforceReadonly compiler option defaults to false. However, by introducing the option now, it becomes possible to gradually update code bases to correct readonly semantics in anticipation of the option possibly becoming part of the --strict family in the future.

Fixes #13347.

ahejlsberg avatar Apr 23 '24 18:04 ahejlsberg