Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 44 additions & 23 deletions src/compiler/checker.ts
Expand Up @@ -1478,6 +1478,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis");
var useUnknownInCatchVariables = getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables");
var exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes;
var enforceReadonly = compilerOptions.enforceReadonly;

var checkBinaryExpression = createCheckBinaryExpression();
var emitResolver = createResolver();
Expand Down Expand Up @@ -23479,9 +23480,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// They're still assignable to one another, since `readonly` doesn't affect assignability.
// This is only applied during the strictSubtypeRelation -- currently used in subtype reduction
if (
relation === strictSubtypeRelation &&
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp)
(relation === strictSubtypeRelation || enforceReadonly) &&
isReadonlySymbol(sourceProp) && !isReadonlySymbol(targetProp) && !(targetProp.flags & SymbolFlags.Method)
) {
if (reportErrors) {
reportError(Diagnostics.Property_0_is_readonly_in_the_source_but_not_in_the_target, symbolToString(targetProp));
}
return Ternary.False;
}
// If the target comes from a partial union prop, allow `undefined` in the target type
Expand Down Expand Up @@ -23940,13 +23944,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function indexInfoRelatedTo(sourceInfo: IndexInfo, targetInfo: IndexInfo, reportErrors: boolean, intersectionState: IntersectionState) {
const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
if (!related && reportErrors) {
if (sourceInfo.keyType === targetInfo.keyType) {
reportError(Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType));
if (!related) {
if (reportErrors) {
if (sourceInfo.keyType === targetInfo.keyType) {
reportError(Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType));
}
else {
reportError(Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType));
}
}
else {
reportError(Diagnostics._0_and_1_index_signatures_are_incompatible, typeToString(sourceInfo.keyType), typeToString(targetInfo.keyType));
return Ternary.False;
}
if (enforceReadonly && sourceInfo.isReadonly && !targetInfo.isReadonly) {
if (reportErrors) {
reportError(Diagnostics._0_index_signature_is_readonly_in_the_source_but_not_in_the_target, typeToString(sourceInfo.keyType));
}
return Ternary.False;
}
return related;
}
Expand Down Expand Up @@ -30968,10 +30981,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getTypeOfPropertyOfContextualType(type: Type, name: __String, nameType?: Type) {
return mapType(type, t => {
if (isGenericMappedType(t) && !t.declaration.nameType) {
const constraint = getConstraintTypeFromMappedType(t);
const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint;
const constraint = getBaseConstraintOrType(getConstraintTypeFromMappedType(t));
const propertyNameType = nameType || getStringLiteralType(unescapeLeadingUnderscores(name));
if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) {
if (isTypeAssignableTo(propertyNameType, constraint)) {
return substituteIndexedMappedType(t, propertyNameType);
}
}
Expand Down Expand Up @@ -31019,25 +31031,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const symbol = getSymbolOfDeclaration(element);
return getTypeOfPropertyOfContextualType(type, symbol.escapedName, getSymbolLinks(symbol).nameType);
}
if (hasDynamicName(element)) {
const name = getNameOfDeclaration(element);
if (name && isComputedPropertyName(name)) {
const exprType = checkExpression(name.expression);
const propType = isTypeUsableAsPropertyName(exprType) && getTypeOfPropertyOfContextualType(type, getPropertyNameFromType(exprType));
if (propType) {
return propType;
}
const name = getNameOfDeclaration(element);
if (name && isComputedPropertyName(name)) {
const exprType = checkExpression(name.expression);
if (isTypeUsableAsPropertyName(exprType)) {
return getTypeOfPropertyOfContextualType(type, getPropertyNameFromType(exprType), exprType);
}
}
if (element.name) {
const nameType = getLiteralTypeFromPropertyName(element.name);
// We avoid calling getApplicableIndexInfo here because it performs potentially expensive intersection reduction.
return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType)?.type, /*noReductions*/ true);
return mapType(type, t => findApplicableIndexInfo(getIndexInfosOfStructuredType(t), exprType)?.type, /*noReductions*/ true);
}
}
return undefined;
}

function isContextualPropertyMutable(type: Type, name: __String, nameType: Type | undefined) {
return someType(type, t => {
const propName = nameType ? isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined : name;
const prop = propName && getPropertyOfType(t, propName);
if (prop) {
return !isReadonlySymbol(prop);
}
const indexInfo = findApplicableIndexInfo(getIndexInfosOfStructuredType(t), nameType || getStringLiteralType(unescapeLeadingUnderscores(name)));
if (indexInfo) {
return !indexInfo.isReadonly;
}
return false;
});
}

function getSpreadIndices(elements: readonly Node[]) {
let first, last;
for (let i = 0; i < elements.length; i++) {
Expand Down Expand Up @@ -31956,7 +31977,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
const inConstContext = isConstContext(node);
const checkFlags = inConstContext ? CheckFlags.Readonly : 0;
const isInJavascript = isInJSFile(node) && !isInJsonFile(node);
const enumTag = isInJavascript ? getJSDocEnumTag(node) : undefined;
const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag;
Expand Down Expand Up @@ -32003,6 +32023,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags;
const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined;
const checkFlags = inConstContext && !(enforceReadonly && contextualType && isContextualPropertyMutable(contextualType, member.escapedName, nameType)) ? CheckFlags.Readonly : 0;
const prop = nameType ?
createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) :
createSymbol(SymbolFlags.Property | member.flags, member.escapedName, checkFlags);
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/commandLineParser.ts
Expand Up @@ -998,6 +998,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
description: Diagnostics.Enforces_using_indexed_accessors_for_keys_declared_using_an_indexed_type,
defaultValueDescription: false,
},
{
name: "enforceReadonly",
type: "boolean",
affectsSemanticDiagnostics: true,
affectsBuildInfo: true,
category: Diagnostics.Type_Checking,
description: Diagnostics.Ensure_that_readonly_properties_remain_read_only_in_type_relationships,
defaultValueDescription: false,
},

// Module Resolution
{
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -4192,6 +4192,14 @@
"category": "Error",
"code": 4126
},
"Property '{0}' is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4127
},
"'{0}' index signature is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4128
},

"The current host does not support the '{0}' option.": {
"category": "Error",
Expand Down Expand Up @@ -6223,6 +6231,10 @@
"category": "Message",
"code": 6718
},
"Ensure that 'readonly' properties remain read-only in type relationships.": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a drive-by comment that as an everyday user of TS, I don't really know what a "type relationship" is. Maybe this could be worded more explicitly as:

Suggested change
"Ensure that 'readonly' properties remain read-only in type relationships.": {
"Enforce that mutable properties cannot satisfy 'readonly' requirements in assignment or subtype relationships.": {

"category": "Message",
"code": 6719
},
"Default catch clause variables as 'unknown' instead of 'any'.": {
"category": "Message",
"code": 6803
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Expand Up @@ -7132,6 +7132,7 @@ export interface CompilerOptions {
downlevelIteration?: boolean;
emitBOM?: boolean;
emitDecoratorMetadata?: boolean;
enforceReadonly?: boolean;
exactOptionalPropertyTypes?: boolean;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
Expand Down
14 changes: 7 additions & 7 deletions src/lib/dom.generated.d.ts
Expand Up @@ -1266,8 +1266,8 @@ interface PushSubscriptionOptionsInit {
}

interface QueuingStrategy<T = any> {
highWaterMark?: number;
size?: QueuingStrategySize<T>;
readonly highWaterMark?: number;
readonly size?: QueuingStrategySize<T>;
}

interface QueuingStrategyInit {
Expand All @@ -1276,7 +1276,7 @@ interface QueuingStrategyInit {
*
* Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw.
*/
highWaterMark: number;
readonly highWaterMark: number;
}

interface RTCAnswerOptions extends RTCOfferAnswerOptions {
Expand Down Expand Up @@ -5965,9 +5965,9 @@ interface DOMException extends Error {
*/
readonly code: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */
readonly message: string;
message: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */
readonly name: string;
name: string;
readonly INDEX_SIZE_ERR: 1;
readonly DOMSTRING_SIZE_ERR: 2;
readonly HIERARCHY_REQUEST_ERR: 3;
Expand Down Expand Up @@ -18625,7 +18625,7 @@ interface ReadableStream<R = any> {

declare var ReadableStream: {
prototype: ReadableStream;
new(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number }): ReadableStream<Uint8Array>;
new(underlyingSource: UnderlyingByteSource, strategy?: { readonly highWaterMark?: number }): ReadableStream<Uint8Array>;
new<R = any>(underlyingSource: UnderlyingDefaultSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;
new<R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;
};
Expand Down Expand Up @@ -19365,7 +19365,7 @@ interface SVGElementEventMap extends ElementEventMap, GlobalEventHandlersEventMa
*/
interface SVGElement extends Element, ElementCSSInlineStyle, GlobalEventHandlers, HTMLOrSVGElement {
/** @deprecated */
readonly className: any;
className: any;
readonly ownerSVGElement: SVGSVGElement | null;
readonly viewportElement: SVGElement | null;
addEventListener<K extends keyof SVGElementEventMap>(type: K, listener: (this: SVGElement, ev: SVGElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/es2015.collection.d.ts
Expand Up @@ -30,7 +30,7 @@ interface Map<K, V> {
interface MapConstructor {
new (): Map<any, any>;
new <K, V>(entries?: readonly (readonly [K, V])[] | null): Map<K, V>;
readonly prototype: Map<any, any>;
prototype: Map<any, any>;
}
declare var Map: MapConstructor;

Expand Down Expand Up @@ -64,7 +64,7 @@ interface WeakMap<K extends WeakKey, V> {

interface WeakMapConstructor {
new <K extends WeakKey = WeakKey, V = any>(entries?: readonly (readonly [K, V])[] | null): WeakMap<K, V>;
readonly prototype: WeakMap<WeakKey, any>;
prototype: WeakMap<WeakKey, any>;
}
declare var WeakMap: WeakMapConstructor;

Expand Down Expand Up @@ -96,7 +96,7 @@ interface Set<T> {

interface SetConstructor {
new <T = any>(values?: readonly T[] | null): Set<T>;
readonly prototype: Set<any>;
prototype: Set<any>;
}
declare var Set: SetConstructor;

Expand Down Expand Up @@ -124,6 +124,6 @@ interface WeakSet<T extends WeakKey> {

interface WeakSetConstructor {
new <T extends WeakKey = WeakKey>(values?: readonly T[] | null): WeakSet<T>;
readonly prototype: WeakSet<WeakKey>;
prototype: WeakSet<WeakKey>;
}
declare var WeakSet: WeakSetConstructor;
4 changes: 2 additions & 2 deletions src/lib/es2015.generator.d.ts
Expand Up @@ -30,7 +30,7 @@ interface GeneratorFunction {
/**
* A reference to the prototype.
*/
readonly prototype: Generator;
prototype: Generator;
}

interface GeneratorFunctionConstructor {
Expand All @@ -55,5 +55,5 @@ interface GeneratorFunctionConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: GeneratorFunction;
prototype: GeneratorFunction;
}
2 changes: 1 addition & 1 deletion src/lib/es2015.promise.d.ts
Expand Up @@ -2,7 +2,7 @@ interface PromiseConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: Promise<any>;
prototype: Promise<any>;

/**
* Creates a new Promise.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2015.symbol.d.ts
Expand Up @@ -2,7 +2,7 @@ interface SymbolConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: Symbol;
prototype: Symbol;

/**
* Returns a new unique Symbol value.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/es2017.object.d.ts
Expand Up @@ -3,7 +3,7 @@ interface ObjectConstructor {
* Returns an array of values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
values<T>(o: { [s: string]: T; } | ArrayLike<T>): T[];
values<T>(o: { readonly [s: string]: T; } | ArrayLike<T>): T[];

/**
* Returns an array of values of the enumerable properties of an object
Expand All @@ -15,7 +15,7 @@ interface ObjectConstructor {
* Returns an array of key/values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][];
entries<T>(o: { readonly [s: string]: T; } | ArrayLike<T>): [string, T][];

/**
* Returns an array of key/values of the enumerable properties of an object
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2017.sharedmemory.d.ts
Expand Up @@ -16,7 +16,7 @@ interface SharedArrayBuffer {
}

interface SharedArrayBufferConstructor {
readonly prototype: SharedArrayBuffer;
prototype: SharedArrayBuffer;
new (byteLength: number): SharedArrayBuffer;
}
declare var SharedArrayBuffer: SharedArrayBufferConstructor;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/es2018.asyncgenerator.d.ts
Expand Up @@ -30,7 +30,7 @@ interface AsyncGeneratorFunction {
/**
* A reference to the prototype.
*/
readonly prototype: AsyncGenerator;
prototype: AsyncGenerator;
}

interface AsyncGeneratorFunctionConstructor {
Expand All @@ -55,5 +55,5 @@ interface AsyncGeneratorFunctionConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: AsyncGeneratorFunction;
prototype: AsyncGeneratorFunction;
}
2 changes: 1 addition & 1 deletion src/lib/es2018.intl.d.ts
Expand Up @@ -32,7 +32,7 @@ declare namespace Intl {
interface PluralRulesConstructor {
new (locales?: string | readonly string[], options?: PluralRulesOptions): PluralRules;
(locales?: string | readonly string[], options?: PluralRulesOptions): PluralRules;
supportedLocalesOf(locales: string | readonly string[], options?: { localeMatcher?: "lookup" | "best fit"; }): string[];
supportedLocalesOf(locales: string | readonly string[], options?: { readonly localeMatcher?: "lookup" | "best fit"; }): string[];
}

const PluralRules: PluralRulesConstructor;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/es2020.bigint.d.ts
Expand Up @@ -104,7 +104,7 @@ interface BigInt {

interface BigIntConstructor {
(value: bigint | boolean | number | string): bigint;
readonly prototype: BigInt;
prototype: BigInt;

/**
* Interprets the low bits of a BigInt as a 2's-complement signed integer.
Expand Down Expand Up @@ -370,7 +370,7 @@ interface BigInt64Array {
}

interface BigInt64ArrayConstructor {
readonly prototype: BigInt64Array;
prototype: BigInt64Array;
new (length?: number): BigInt64Array;
new (array: Iterable<bigint>): BigInt64Array;
new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): BigInt64Array;
Expand Down Expand Up @@ -642,7 +642,7 @@ interface BigUint64Array {
}

interface BigUint64ArrayConstructor {
readonly prototype: BigUint64Array;
prototype: BigUint64Array;
new (length?: number): BigUint64Array;
new (array: Iterable<bigint>): BigUint64Array;
new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): BigUint64Array;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2020.intl.d.ts
Expand Up @@ -426,7 +426,7 @@ declare namespace Intl {
*
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/supportedLocalesOf).
*/
supportedLocalesOf(locales?: LocalesArgument, options?: { localeMatcher?: RelativeTimeFormatLocaleMatcher; }): UnicodeBCP47LocaleIdentifier[];
supportedLocalesOf(locales?: LocalesArgument, options?: { readonly localeMatcher?: RelativeTimeFormatLocaleMatcher; }): UnicodeBCP47LocaleIdentifier[];
};

interface CollatorConstructor {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/es2021.promise.d.ts
Expand Up @@ -5,7 +5,7 @@ interface AggregateError extends Error {
interface AggregateErrorConstructor {
new (errors: Iterable<any>, message?: string): AggregateError;
(errors: Iterable<any>, message?: string): AggregateError;
readonly prototype: AggregateError;
prototype: AggregateError;
}

declare var AggregateError: AggregateErrorConstructor;
Expand Down