diff --git a/packages/common/core-interfaces/src/deepReadonly.ts b/packages/common/core-interfaces/src/deepReadonly.ts new file mode 100644 index 000000000000..33c66dd7dcc6 --- /dev/null +++ b/packages/common/core-interfaces/src/deepReadonly.ts @@ -0,0 +1,76 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { + DeepReadonlyRecursionLimit, + InternalUtilityTypes, + ReadonlySupportedGenerics, +} from "./exposedInternalUtilityTypes.js"; + +/** + * Default set of generic that {@link DeepReadonly} will apply deep immutability + * to generic types. + * + * @privateRemarks + * WeakRef should be added when lib is updated to ES2021 or later. + * + * @system + */ +export type DeepReadonlySupportedGenericsDefault = + | Map + | Promise + | Set + | WeakMap + | WeakSet; + +/** + * Options for {@link DeepReadonly}. + * + * @beta + */ +export interface DeepReadonlyOptions { + /** + * Union of Built-in and IFluidHandle whose generics will also be made deeply immutable. + * + * The default value is `Map` | `Promise` | `Set` | `WeakMap` | `WeakSet`. + */ + DeepenedGenerics?: ReadonlySupportedGenerics; + + /** + * Limit on processing recursive types. + * + * The default value is `"NoLimit"`. + */ + RecurseLimit?: DeepReadonlyRecursionLimit; +} + +/** + * Transforms type to a fully and deeply immutable type, with limitations. + * + * @remarks + * This utility type is similar to a recursive `Readonly`, but also + * applies immutability to common generic types like `Map` and `Set`. + * + * Optionally, immutability can be applied to supported generics types. See + * {@link DeepReadonlySupportedGenericsDefault} for generics that have + * immutability applied to generic type by default. + * + * @beta + */ +export type DeepReadonly< + T, + Options extends DeepReadonlyOptions = { + DeepenedGenerics: DeepReadonlySupportedGenericsDefault; + RecurseLimit: "NoLimit"; + }, +> = InternalUtilityTypes.DeepReadonlyImpl< + T, + Options extends { DeepenedGenerics: unknown } + ? Options["DeepenedGenerics"] + : DeepReadonlySupportedGenericsDefault, + Options extends { RecurseLimit: DeepReadonlyRecursionLimit } + ? Options["RecurseLimit"] + : "NoLimit" +>; diff --git a/packages/common/core-interfaces/src/exposedInternalUtilityTypes.ts b/packages/common/core-interfaces/src/exposedInternalUtilityTypes.ts index 44572588a734..4fd2c790c097 100644 --- a/packages/common/core-interfaces/src/exposedInternalUtilityTypes.ts +++ b/packages/common/core-interfaces/src/exposedInternalUtilityTypes.ts @@ -5,17 +5,51 @@ /* eslint-disable @rushstack/no-new-null */ +import type { ErasedType } from "./erasedType.js"; +import type { IFluidHandle } from "./handles.js"; import type { SerializationErrorPerNonPublicProperties, SerializationErrorPerUndefinedArrayElement, } from "./jsonSerializationErrors.js"; -import type { JsonTypeWith, NonNullJsonObjectWith } from "./jsonType.js"; +import type { JsonTypeWith, NonNullJsonObjectWith, ReadonlyJsonTypeWith } from "./jsonType.js"; /** * Unique symbol for recursion meta-typing. */ const RecursionMarkerSymbol: unique symbol = Symbol("recursion here"); +/** + * Union of types that {@link DeepReadonly} and {@link ShallowReadonly} + * recognize to generate immutable form and optionally can alter their + * type parameters. + * + * @privateRemarks + * WeakRef should be added when lib is updated to ES2021 or later. + * + * @beta + */ +export type ReadonlySupportedGenerics = + | IFluidHandle + | Map + | Promise + | Set + | WeakMap + | WeakSet; + +/** + * Limit on processing recursive types. + * Use of `"NoLimit"` may result in error: + * "ts(2589): Type instantiation is excessively deep and possibly infinite". + * In such cases, use string literal with some prefix series of `+` + * characters. The length of `+` character sequence indicates the recursion + * depth limit when a recursive type is found. Use of `0` will stop applying + * `DeepReadonly` at the first point recursion is detected. + * + * @beta + * @system + */ +export type DeepReadonlyRecursionLimit = "NoLimit" | 0 | `+${string}`; + /** * Collection of utility types that are not intended to be used/imported * directly outside of this package. @@ -936,8 +970,6 @@ export namespace InternalUtilityTypes { // #endregion - // #region JsonDeserialized implementation - /** * Sentinel type for use when marking points of recursion (in a recursive type). * Type is expected to be unique, though no lengths are taken to ensure that. @@ -955,12 +987,14 @@ export namespace InternalUtilityTypes { */ export type RecursionLimit = `+${string}` | 0; + // #region JsonDeserialized implementation + /** * Outer implementation of {@link JsonDeserialized} handling meta cases * like recursive types. * * @privateRemarks - * This utility is reentrant and will process a type `T` up to RecurseLimit. + * This utility is reentrant and will process a type `T` up to `RecurseLimit`. * * @system */ @@ -1046,7 +1080,7 @@ export namespace InternalUtilityTypes { : never /* unreachable else for infer */; /** - * Recurses T applying {@link InternalUtilityTypes.JsonDeserializedFilter} up to RecurseLimit times. + * Recurses `T` applying {@link InternalUtilityTypes.JsonDeserializedFilter} up to `RecurseLimit` times. * * @system */ @@ -1149,4 +1183,417 @@ export namespace InternalUtilityTypes { : /* not an object => */ never; // #endregion + + // #region *Readonly implementations + + /** + * If `T` is a `Map` or `ReadonlyMap`, returns `ReadonlyMap` and, + * if `T` extends `DeepenedGenerics`, {@link DeepReadonly} is applied to + * `K` and `V` generics. `Else` is returned if not a generic map. + * + * @system + */ + export type DeepenReadonlyInMapIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends ReadonlyMap + ? Map extends DeepenedGenerics + ? ReadonlyMap< + ReadonlyImpl, + ReadonlyImpl + > + : ReadonlyMap + : Else; + + /** + * If `T` is a `Set` or `ReadonlySet`, returns `ReadonlySet` and, + * if `T` extends `DeepenedGenerics`, {@link DeepReadonly} is applied to + * `TSet` generic. `Else` is returned if not a generic set. + * + * @system + */ + export type DeepenReadonlyInSetIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends ReadonlySet + ? Set extends DeepenedGenerics + ? ReadonlySet> + : ReadonlySet + : Else; + + /** + * If `T` is a `IFluidHandle` and if `T` extends `DeepenedGenerics`, + * {@link DeepReadonly} is applied to `THandle` generic. + * `Else` is returned if not an `IFluidHandle`. + * + * @system + */ + export type DeepenReadonlyInFluidHandleIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends Readonly> + ? IFluidHandle extends DeepenedGenerics + ? Readonly>> + : Readonly + : Else; + + /** + * If `T` is a `Promise` and if `T` extends `DeepenedGenerics`, + * {@link DeepReadonly} is applied to `TPromise` generic. + * `Else` is returned if not a `Promise`. + * + * @system + */ + export type DeepenReadonlyInPromiseIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends Promise + ? Promise extends DeepenedGenerics + ? Promise> + : T + : Else; + + /** + * If `T` is a `WeakMap`, returns immutable `WeakMap` and, + * if `T` extends `DeepenedGenerics`, {@link DeepReadonly} is applied to + * `K` and `V` generics. `Else` is returned if not a generic map. + * + * @system + */ + export type DeepenReadonlyInWeakMapIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends Omit, "delete" | "set"> + ? WeakMap extends DeepenedGenerics + ? Omit< + WeakMap< + ReadonlyImpl, + ReadonlyImpl + >, + "delete" | "set" + > + : Omit, "delete" | "set"> + : Else; + + /** + * If `T` is a `WeakSet`, returns immutable `WeakSet` and, + * if `T` extends `DeepenedGenerics`, {@link DeepReadonly} is applied to + * `TSet` generic. `Else` is returned if not a generic weak set. + * + * @system + */ + export type DeepenReadonlyInWeakSetIfEnabled< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = T extends Omit, "add" | "delete"> + ? WeakSet extends DeepenedGenerics + ? Omit< + WeakSet>, + "add" | "delete" + > + : Omit, "add" | "delete"> + : Else; + + /** + * If `T` is a {@link ReadonlySupportedGenerics}, `T` is returned as + * its immutable version, and if `T` extends `DeepenedGenerics`, + * {@link DeepReadonly} is applied to `T`s generics. + * + * @system + */ + export type DeepenReadonlyInGenerics< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + Else, + > = DeepenReadonlyInMapIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + DeepenReadonlyInSetIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + DeepenReadonlyInWeakMapIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + DeepenReadonlyInWeakSetIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + DeepenReadonlyInPromiseIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + DeepenReadonlyInFluidHandleIfEnabled< + T, + DeepenedGenerics, + NoDepthOrRecurseLimit, + Else + > + > + > + > + > + >; + + /** + * Returns an `ErasedType` or "branded" primitive type as-is, or `Else` if not. + * @typeParam T - Type to test. + * @typeParam Else - Type to return if not `ErasedType` or "branded" primitive. + * + * @system + */ + export type PreserveErasedTypeOrBrandedPrimitive< + T extends object, + Else, + > = /* Test for erased type */ T extends ErasedType + ? /* erased type => keep as-is */ T + : /* Test for branded primitive */ T extends infer Brand & + (boolean | number | string | symbol | bigint) + ? // Should just return T here, but TypeScript appears to produce `never` when doing so. + // Workaround by inferring the Primitive type and returning intersection with B. + T extends Brand & infer Primitive + ? /* [potentially] branded type => "as-is" */ Primitive & Brand + : /* Should never be reached */ T + : Else; + + /** + * De-multiplexing implementation of {@link DeepReadonly} and {@link ShallowReadonly} + * selecting behavior based on `NoDepthOrRecurseLimit`. + * + * @privateRemarks + * This utility is reentrant. Its importance is that other utilities common to + * readonly transformation may use `NoDepthOrRecurseLimit` to return the the + * same processing algorithm. + * + * @system + */ + export type ReadonlyImpl< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends "Shallow" | DeepReadonlyRecursionLimit, + > = /* test for no depth */ NoDepthOrRecurseLimit extends "Shallow" + ? /* no depth => */ ShallowReadonlyImpl + : /* test for no limit */ NoDepthOrRecurseLimit extends "NoLimit" + ? /* no limit => */ DeepReadonlyRecursingInfinitely + : /* limited */ DeepReadonlyLimitingRecursion< + T, + DeepenedGenerics, + Extract + >; + + // #region ShallowReadonly implementation + + /** + * Outer implementation of {@link ShallowReadonly}. + * + * @privateRemarks + * This utility can be reentrant when generics are deepened. + * + * @system + */ + export type ShallowReadonlyImpl< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + > = T extends object + ? /* object => */ FilterPreservingFunction< + T, + DeepenReadonlyInGenerics< + T, + DeepenedGenerics, + "Shallow", + PreserveErasedTypeOrBrandedPrimitive */ Readonly> + > + > + : /* not an object => */ T; + + // #endregion + + /** + * Outer implementation of {@link DeepReadonly}. + * + * @privateRemarks + * This utility is reentrant and will process a type `T` up to `RecurseLimit`. + * + * @system + */ + export type DeepReadonlyImpl< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + RecurseLimit extends DeepReadonlyRecursionLimit, + > = ReadonlyImpl; + + // #region DeepReadonly infinite recursion implementation + + /** + * Simple implementation of {@link DeepReadonly} that may handle limited recursive types. + * In unhandled cases, TypeScript will produce: + * ts(2589): Type instantiation is excessively deep and possibly infinite. + * + * @system + */ + export type DeepReadonlyRecursingInfinitely< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + > = T extends object + ? /* object => */ FilterPreservingFunction< + T, + // test for array of known recursive type: JsonTypeWith + T extends readonly ReadonlyJsonTypeWith[] + ? // Make sure this is exactly that case (many arrays will extend JsonTypeWith) + // Note that `true extends IfExactTypeInTuple` is used over + // if-else form as the else branch would be evaluated as input to + // `IfExactTypeInTuple` and that could lead to infinite recursion. + true extends IfExactTypeInTuple< + T, + [JsonTypeWith[], readonly ReadonlyJsonTypeWith[]] + > + ? readonly ReadonlyJsonTypeWith< + DeepReadonlyRecursingInfinitely + >[] + : { + readonly [K in keyof T]: DeepReadonlyRecursingInfinitely< + T[K], + DeepenedGenerics + >; + } + : /* not JSON array => */ DeepenReadonlyInGenerics< + T, + DeepenedGenerics, + "NoLimit", + PreserveErasedTypeOrBrandedPrimitive< + T, + /* basic type => */ { + readonly [K in keyof T]: DeepReadonlyRecursingInfinitely< + T[K], + DeepenedGenerics + >; + } + > + > + > + : /* not an object => */ T; + + // #endregion + // #region DeepReadonly limited recursion implementation + + /** + * Core implementation of {@link DeepReadonly} handling meta cases + * like recursive types a limited number of times. + * + * @privateRemarks + * This utility is reentrant and will process a type `T` up to `NoDepthOrRecurseLimit`. + * + * @system + */ + export type DeepReadonlyLimitingRecursion< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + NoDepthOrRecurseLimit extends RecursionLimit, + > = /* infer non-recursive version of T */ ReplaceRecursionWithMarkerAndPreserveAllowances< + T, + RecursionMarker, + { AllowExactly: []; AllowExtensionOf: never } + > extends infer TNoRecursionAndOnlyPublics + ? /* test for no change from altered type (excluding non-publics) */ IsSameType< + TNoRecursionAndOnlyPublics, + DeepReadonlyWorker + > extends true + ? /* same (no filtering needed) => test for non-public properties (class instance type) */ + IfNonPublicProperties< + T, + // Note: no extra allowance is made here for possible branded + // primitives as DeepReadonlyWorker will allow them as + // extensions of the primitives. Should there need a need to + // explicit allow them here, see JsonSerializableImpl's use. + { AllowExactly: []; AllowExtensionOf: never }, + "found non-publics", + "only publics" + > extends "found non-publics" + ? /* hidden props => apply filtering => */ + DeepReadonlyWorker< + T, + DeepenedGenerics, + Extract + > + : /* no hidden properties => readonly T is just T */ + T + : /* filtering is needed => */ DeepReadonlyWorker< + T, + DeepenedGenerics, + Extract + > + : /* unreachable else for infer */ never; + + /** + * Recurses `T` applying {@link InternalUtilityTypes.DeepReadonlyWorker} up to `RecurseLimit` times. + * + * @system + */ + export type DeepReadonlyRecursion< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + RecurseLimit extends RecursionLimit, + TAncestorTypes = T /* Always start with self as ancestor; otherwise recursion limit appears one greater */, + > = T extends TAncestorTypes + ? RecurseLimit extends `+${infer RecursionRemainder}` + ? /* Now that specific recursion is found, process that recursive type + directly to avoid any collateral damage from ancestor type that + required modification. */ + DeepReadonlyLimitingRecursion< + T, + DeepenedGenerics, + RecursionRemainder extends RecursionLimit ? RecursionRemainder : 0 + > + : T + : DeepReadonlyWorker; + + /** + * Core implementation of {@link InternalUtilityTypes.DeepReadonlyLimitingRecursion}. + * + * @system + */ + export type DeepReadonlyWorker< + T, + DeepenedGenerics extends ReadonlySupportedGenerics, + RecurseLimit extends RecursionLimit, + TAncestorTypes = T /* Always start with self as ancestor; otherwise recursion limit appears one greater */, + > = /* test for object */ T extends object + ? /* object => */ FilterPreservingFunction< + T, + DeepenReadonlyInGenerics< + T, + DeepenedGenerics, + RecurseLimit, + PreserveErasedTypeOrBrandedPrimitive< + T, + /* basic type => */ { + readonly [K in keyof T]: DeepReadonlyRecursion< + T[K], + DeepenedGenerics, + RecurseLimit, + TAncestorTypes + >; + } + > + > + > + : /* not an object => */ T; + + // #endregion + // #endregion } diff --git a/packages/common/core-interfaces/src/exposedUtilityTypes.ts b/packages/common/core-interfaces/src/exposedUtilityTypes.ts index 925b22b185bc..9f06dff06362 100644 --- a/packages/common/core-interfaces/src/exposedUtilityTypes.ts +++ b/packages/common/core-interfaces/src/exposedUtilityTypes.ts @@ -9,12 +9,21 @@ // Should a customer need access to these types, export should be relocated to // index.ts and retagged export from internal.ts may be removed. +export type { DeepReadonly } from "./deepReadonly.js"; export type { JsonDeserialized, JsonDeserializedOptions } from "./jsonDeserialized.js"; export type { JsonSerializable, JsonSerializableOptions } from "./jsonSerializable.js"; export type { SerializationErrorPerNonPublicProperties, SerializationErrorPerUndefinedArrayElement, } from "./jsonSerializationErrors.js"; -export type { JsonTypeWith, NonNullJsonObjectWith } from "./jsonType.js"; +export type { + JsonTypeWith, + NonNullJsonObjectWith, + ReadonlyJsonTypeWith, +} from "./jsonType.js"; +export type { ShallowReadonly } from "./shallowReadonly.js"; -export type { InternalUtilityTypes } from "./exposedInternalUtilityTypes.js"; +export type { + InternalUtilityTypes, + ReadonlySupportedGenerics, +} from "./exposedInternalUtilityTypes.js"; diff --git a/packages/common/core-interfaces/src/internal.ts b/packages/common/core-interfaces/src/internal.ts index c0c536e5f4f9..35090d64c0eb 100644 --- a/packages/common/core-interfaces/src/internal.ts +++ b/packages/common/core-interfaces/src/internal.ts @@ -14,6 +14,7 @@ export * from "./index.js"; // These types are not intended for direct use by customers and api-extractor will // flag misuse. If an externally visible version of these types is needed, import // from via /internal/exposedUtilityTypes rather than /internal. +import type { DeepReadonly as ExposedDeepReadonly } from "./deepReadonly.js"; import type { InternalUtilityTypes as ExposedInternalUtilityTypes } from "./exposedInternalUtilityTypes.js"; import type { JsonDeserialized as ExposedJsonDeserialized, @@ -23,13 +24,21 @@ import type { JsonSerializable as ExposedJsonSerializable, JsonSerializableOptions, } from "./jsonSerializable.js"; -import type { JsonTypeWith as ExposedJsonTypeWith } from "./jsonType.js"; +import type { + JsonTypeWith as ExposedJsonTypeWith, + ReadonlyNonNullJsonObjectWith as ExposedReadonlyNonNullJsonObjectWith, +} from "./jsonType.js"; // Note: There are no docs for these re-exports. `@inheritdoc` cannot be used as: // 1. api-extractor does not support renames. // 2. api-extractor does not support package paths. ("Import paths are not supported") // Also not useful, at least in VS Code, as substitution is not made in place. +/** + * @internal + */ +export type DeepReadonly = ExposedDeepReadonly; + /** * @internal */ @@ -57,6 +66,11 @@ export type JsonSerializable< */ export type JsonTypeWith = ExposedJsonTypeWith; +/** + * @internal + */ +export type ReadonlyNonNullJsonObjectWith = ExposedReadonlyNonNullJsonObjectWith; + /** * @internal */ diff --git a/packages/common/core-interfaces/src/jsonType.ts b/packages/common/core-interfaces/src/jsonType.ts index 726e46fb01f4..0ebd97999ffd 100644 --- a/packages/common/core-interfaces/src/jsonType.ts +++ b/packages/common/core-interfaces/src/jsonType.ts @@ -35,3 +35,39 @@ export type JsonTypeWith = export type NonNullJsonObjectWith = | { [key: string | number]: JsonTypeWith } | JsonTypeWith[]; + +/** + * Deeply immutable type that is encodable as JSON and deserializable from JSON. + * + * @typeParam TReadonlyAlternates - Additional [immutable] types that are supported. + * + * @remarks + * If `TReadonlyAlternates` is allowed as-is. So if it is not immutable, then result type + * is not wholly immutable. + * + * A `const` variable is still required to avoid top-level mutability. I.e. + * ```typescript + * let x: ReadonlyJsonTypeWith = { a: 1 }; + * ``` + * does not prevent later `x = 5`. (Does prevent `x.a = 2`.) + * + * @beta + */ +export type ReadonlyJsonTypeWith = + // eslint-disable-next-line @rushstack/no-new-null + | null + | boolean + | number + | string + | TReadonlyAlternates + | { readonly [key: string | number]: ReadonlyJsonTypeWith } + | readonly ReadonlyJsonTypeWith[]; + +/** + * Portion of {@link ReadonlyJsonTypeWith} that is an object (including array) and not null. + * + * @internal + */ +export type ReadonlyNonNullJsonObjectWith = + | { readonly [key: string | number]: ReadonlyJsonTypeWith } + | readonly ReadonlyJsonTypeWith[]; diff --git a/packages/common/core-interfaces/src/shallowReadonly.ts b/packages/common/core-interfaces/src/shallowReadonly.ts new file mode 100644 index 000000000000..009668b19842 --- /dev/null +++ b/packages/common/core-interfaces/src/shallowReadonly.ts @@ -0,0 +1,60 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { + InternalUtilityTypes, + ReadonlySupportedGenerics, +} from "./exposedInternalUtilityTypes.js"; +import type { IFluidHandle } from "./handles.js"; + +/** + * Default set of generic that {@link ShallowReadonly} will apply shallow immutability + * to generic types. + * + * @privateRemarks + * WeakRef should be added when lib is updated to ES2021 or later. + * + * @system + */ +export type ShallowReadonlySupportedGenericsDefault = Promise | IFluidHandle; + +/** + * Options for {@link ShallowReadonly}. + * + * @beta + */ +export interface ShallowReadonlyOptions { + /** + * Union of Built-in and IFluidHandle whose generics will also be made shallowly immutable. + * + * The default value is `IFluidHandle` | `Promise`. + */ + DeepenedGenerics?: ReadonlySupportedGenerics; +} + +/** + * Transforms type to a shallowly immutable type. + * + * @remarks + * This utility type is similar to `Readonly`, but also applies immutability to + * common generic types like `Map` and `Set`. + * + * Optionally, immutability can be applied to supported generics types. See + * {@link ShallowReadonlySupportedGenericsDefault} for generics that have + * immutability applied to generic type by default. + * + * @beta + */ +export type ShallowReadonly< + T, + Options extends ShallowReadonlyOptions = { + DeepenedGenerics: ShallowReadonlySupportedGenericsDefault; + }, +> = InternalUtilityTypes.ShallowReadonlyImpl< + T, + Options extends { DeepenedGenerics: unknown } + ? Options["DeepenedGenerics"] + : ShallowReadonlySupportedGenericsDefault +>; diff --git a/packages/common/core-interfaces/src/test/deepReadonly.spec.ts b/packages/common/core-interfaces/src/test/deepReadonly.spec.ts new file mode 100644 index 000000000000..969a3b7bbdcb --- /dev/null +++ b/packages/common/core-interfaces/src/test/deepReadonly.spec.ts @@ -0,0 +1,1340 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { assertIdenticalTypes, createInstanceOf } from "./testUtils.js"; +import type { + ClassWithPublicData, + Point, + ReadonlyObjectWithOptionalRecursion, +} from "./testValues.js"; +import { + boolean, + number, + string, + numericEnumValue, + NumericEnum, + stringEnumValue, + StringEnum, + constHeterogenousEnumValue, + ConstHeterogenousEnum, + computedEnumValue, + ComputedEnum, + objectWithLiterals, + arrayOfLiterals, + tupleWithLiterals, + symbol, + uniqueSymbol, + bigint, + aFunction, + unknownValueOfSimpleRecord, + voidValue, + never, + stringOrSymbol, + bigintOrString, + bigintOrSymbol, + numberOrBigintOrSymbol, + functionWithProperties, + objectAndFunction, + arrayOfNumbers, + arrayOfNumbersSparse, + arrayOfNumbersOrUndefined, + arrayOfBigints, + arrayOfSymbols, + arrayOfFunctions, + arrayOfFunctionsWithProperties, + arrayOfObjectAndFunctions, + arrayOfBigintOrSymbols, + arrayOfNumberBigintOrSymbols, + arrayOfBigintOrObjects, + arrayOfSymbolOrObjects, + readonlyArrayOfNumbers, + readonlyArrayOfObjects, + object, + emptyObject, + objectWithBoolean, + objectWithNumber, + objectWithString, + objectWithSymbol, + objectWithBigint, + objectWithFunction, + objectWithFunctionWithProperties, + objectWithObjectAndFunction, + objectWithBigintOrString, + objectWithBigintOrSymbol, + objectWithNumberOrBigintOrSymbol, + objectWithFunctionOrSymbol, + objectWithStringOrSymbol, + objectWithUnknown, + objectWithOptionalUnknown, + objectWithUndefined, + objectWithOptionalUndefined, + objectWithOptionalBigint, + objectWithNumberKey, + objectWithSymbolKey, + objectWithUniqueSymbolKey, + objectWithArrayOfNumbers, + objectWithArrayOfNumbersOrUndefined, + objectWithArrayOfBigints, + objectWithArrayOfSymbols, + objectWithArrayOfUnknown, + objectWithArrayOfFunctions, + objectWithArrayOfFunctionsWithProperties, + objectWithArrayOfObjectAndFunctions, + objectWithArrayOfBigintOrObjects, + objectWithArrayOfSymbolOrObjects, + objectWithReadonlyArrayOfNumbers, + objectWithOptionalNumberNotPresent, + objectWithNumberOrUndefinedUndefined, + objectWithReadonly, + objectWithReadonlyViaGetter, + objectWithGetter, + objectWithGetterViaValue, + objectWithSetter, + objectWithMatchedGetterAndSetterProperty, + objectWithMatchedGetterAndSetterPropertyViaValue, + objectWithMismatchedGetterAndSetterProperty, + objectWithMismatchedGetterAndSetterPropertyViaValue, + objectWithNever, + stringRecordOfNumbers, + stringRecordOfUnknown, + stringOrNumberRecordOfStrings, + stringOrNumberRecordOfObjects, + partialStringRecordOfNumbers, + partialStringRecordOfUnknown, + templatedRecordOfNumbers, + mixedRecordOfUnknown, + stringRecordOfNumbersOrStringsWithKnownProperties, + stringRecordOfUnknownWithOptionalKnownProperties, + stringOrNumberRecordOfStringWithKnownNumber, + objectWithPossibleRecursion, + objectWithOptionalRecursion, + readonlyObjectWithOptionalRecursion, + objectWithEmbeddedRecursion, + readonlyObjectWithEmbeddedRecursion, + objectWithAlternatingRecursion, + readonlyObjectWithAlternatingRecursion, + objectWithSymbolOrRecursion, + readonlyObjectWithSymbolOrRecursion, + objectWithFluidHandleOrRecursion, + stringRecordWithRecursionOrNumber, + readonlyStringRecordWithRecursionOrNumber, + selfRecursiveFunctionWithProperties, + selfRecursiveObjectAndFunction, + readonlySelfRecursiveFunctionWithProperties, + readonlySelfRecursiveObjectAndFunction, + objectInheritingOptionalRecursionAndWithNestedSymbol, + simpleJson, + simpleImmutableJson, + jsonObject, + immutableJsonObject, + classInstanceWithPrivateData, + classInstanceWithPrivateMethod, + classInstanceWithPrivateGetter, + classInstanceWithPrivateSetter, + classInstanceWithPublicData, + classInstanceWithPublicMethod, + functionObjectWithPrivateData, + functionObjectWithPublicData, + classInstanceWithPrivateDataAndIsFunction, + classInstanceWithPublicDataAndIsFunction, + mapOfStringsToNumbers, + readonlyMapOfStringsToNumbers, + mapOfPointToRecord, + readonlyMapOfPointToRecord, + setOfNumbers, + readonlySetOfNumbers, + setOfRecords, + readonlySetOfRecords, + brandedNumber, + brandedString, + brandedObject, + brandedObjectWithString, + objectWithBrandedNumber, + objectWithBrandedString, + fluidHandleToNumber, + fluidHandleToRecord, + objectWithFluidHandle, + readonlyObjectWithFluidHandleOrRecursion, + erasedType, +} from "./testValues.js"; + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import type { DeepReadonly } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +/** + * Result is defined using `DeepReadonly` type generator. + * + * @param v - value whose type is passed through `DeepReadonly` + * @returns the original value with modified type + */ +function makeReadonly(v: T) { + return v as DeepReadonly; +} + +function makeReadonlyDeepeningHandleTypes(v: T) { + return v as DeepReadonly; +} + +function makeReadonlyNoGenericsDeepening(v: T) { + return v as DeepReadonly; +} + +function makeReadonlyBailingOnRecursiveTypes(v: T) { + return v as DeepReadonly; +} + +function makeReadonlyWithRecurseLimitThree(v: T) { + return v as DeepReadonly; +} + +/* eslint-enable @typescript-eslint/explicit-function-return-type */ + +describe("DeepReadonly", () => { + describe("primitive types are preserved", () => { + it("`undefined`", () => { + const result = makeReadonly(undefined); + assertIdenticalTypes(result, undefined); + }); + it("`boolean`", () => { + const result = makeReadonly(boolean); + assertIdenticalTypes(result, boolean); + }); + it("`number`", () => { + const result = makeReadonly(number); + assertIdenticalTypes(result, number); + }); + it("`string`", () => { + const result = makeReadonly(string); + assertIdenticalTypes(result, string); + }); + it("`symbol`", () => { + const result = makeReadonly(symbol); + assertIdenticalTypes(result, symbol); + }); + it("`bigint`", () => { + const result = makeReadonly(bigint); + assertIdenticalTypes(result, bigint); + }); + it("function", () => { + const result = makeReadonly(aFunction); + assertIdenticalTypes(result, aFunction); + }); + it("numeric enum", () => { + const result = makeReadonly(numericEnumValue); + assertIdenticalTypes(result, numericEnumValue); + }); + it("string enum", () => { + const result = makeReadonly(stringEnumValue); + assertIdenticalTypes(result, stringEnumValue); + }); + it("const heterogenous enum", () => { + const result = makeReadonly(constHeterogenousEnumValue); + assertIdenticalTypes(result, constHeterogenousEnumValue); + }); + it("computed enum", () => { + const result = makeReadonly(computedEnumValue); + assertIdenticalTypes(result, computedEnumValue); + }); + }); + + describe("unions of primitive types are preserved", () => { + it("`string | symbol`", () => { + const result = makeReadonly(stringOrSymbol); + assertIdenticalTypes(result, stringOrSymbol); + }); + it("`bigint | string`", () => { + const result = makeReadonly(bigintOrString); + assertIdenticalTypes(result, bigintOrString); + }); + it("`bigint | symbol`", () => { + const result = makeReadonly(bigintOrSymbol); + assertIdenticalTypes(result, bigintOrSymbol); + }); + it("`number | bigint | symbol`", () => { + const result = makeReadonly(numberOrBigintOrSymbol); + assertIdenticalTypes(result, numberOrBigintOrSymbol); + }); + }); + + describe("literal types are preserved", () => { + it("`true`", () => { + const result = makeReadonly(true as const); + assertIdenticalTypes(result, true); + }); + it("`false`", () => { + const result = makeReadonly(false as const); + assertIdenticalTypes(result, false); + }); + it("`0`", () => { + const result = makeReadonly(0 as const); + assertIdenticalTypes(result, 0); + }); + it('"string"', () => { + const result = makeReadonly("string" as const); + assertIdenticalTypes(result, "string"); + }); + it("object with literals", () => { + const result = makeReadonly(objectWithLiterals); + assertIdenticalTypes(result, objectWithLiterals); + }); + it("array of literals", () => { + const result = makeReadonly(arrayOfLiterals); + assertIdenticalTypes(result, arrayOfLiterals); + }); + it("tuple of literals", () => { + const result = makeReadonly(tupleWithLiterals); + assertIdenticalTypes(result, tupleWithLiterals); + }); + it("specific numeric enum value", () => { + const result = makeReadonly(NumericEnum.two as const); + assertIdenticalTypes(result, NumericEnum.two); + }); + it("specific string enum value", () => { + const result = makeReadonly(StringEnum.b as const); + assertIdenticalTypes(result, StringEnum.b); + }); + it("specific const heterogenous enum value", () => { + const result = makeReadonly(ConstHeterogenousEnum.zero as const); + assertIdenticalTypes(result, ConstHeterogenousEnum.zero); + }); + it("specific computed enum value", () => { + const result = makeReadonly(ComputedEnum.computed as const); + assertIdenticalTypes(result, ComputedEnum.computed); + }); + }); + + describe("arrays become immutable", () => { + it("array of numbers", () => { + const result = makeReadonly(arrayOfNumbers); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("array of numbers with holes", () => { + const result = makeReadonly(arrayOfNumbersSparse); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("array of numbers or undefined", () => { + const result = makeReadonly(arrayOfNumbersOrUndefined); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of bigint or basic object", () => { + const result = makeReadonly(arrayOfBigintOrObjects); + assertIdenticalTypes( + result, + createInstanceOf(), + ); + }); + it("array of supported types (symbols or basic object)", () => { + const result = makeReadonly(arrayOfSymbolOrObjects); + assertIdenticalTypes( + result, + createInstanceOf(), + ); + }); + it("array of bigint", () => { + const result = makeReadonly(arrayOfBigints); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of symbols", () => { + const result = makeReadonly(arrayOfSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of functions", () => { + const result = makeReadonly(arrayOfFunctions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assertIdenticalTypes(result, createInstanceOf any)[]>()); + }); + it("array of functions with properties", () => { + const result = makeReadonly(arrayOfFunctionsWithProperties); + assertIdenticalTypes( + result, + createInstanceOf number) & { readonly property: number })[]>(), + ); + }); + it("array of objects and functions", () => { + const result = makeReadonly(arrayOfObjectAndFunctions); + assertIdenticalTypes( + result, + createInstanceOf number))[]>(), + ); + }); + it("array of `bigint | symbol`", () => { + const result = makeReadonly(arrayOfBigintOrSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of `number | bigint | symbol`", () => { + const result = makeReadonly(arrayOfNumberBigintOrSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + }); + + describe("read-only arrays are preserved", () => { + it("readonly array of primitive is preserved", () => { + const result = makeReadonly(readonlyArrayOfNumbers); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("readonly array of mutable object becomes deeply immutable", () => { + const result = makeReadonly(readonlyArrayOfObjects); + assertIdenticalTypes( + result, + createInstanceOf(), + ); + }); + }); + + describe("object properties become immutable", () => { + it("empty object", () => { + const result = makeReadonly(emptyObject); + assertIdenticalTypes(result, emptyObject); + }); + + it("object with `boolean`", () => { + const result = makeReadonly(objectWithBoolean); + assertIdenticalTypes(result, createInstanceOf<{ readonly boolean: boolean }>()); + }); + it("object with `number`", () => { + const result = makeReadonly(objectWithNumber); + assertIdenticalTypes(result, createInstanceOf<{ readonly number: number }>()); + }); + it("object with `string`", () => { + const result = makeReadonly(objectWithString); + assertIdenticalTypes(result, createInstanceOf<{ readonly string: string }>()); + }); + it("object with `bigint`", () => { + const result = makeReadonly(objectWithBigint); + assertIdenticalTypes(result, createInstanceOf<{ readonly bigint: bigint }>()); + }); + it("object with `symbol`", () => { + const result = makeReadonly(objectWithSymbol); + assertIdenticalTypes(result, createInstanceOf<{ readonly symbol: symbol }>()); + }); + it("object with function", () => { + const result = makeReadonly(objectWithFunction); + assertIdenticalTypes(result, createInstanceOf<{ readonly function: () => void }>()); + }); + it("object with `unknown`", () => { + const result = makeReadonly(objectWithUnknown); + assertIdenticalTypes(result, createInstanceOf<{ readonly unknown: unknown }>()); + }); + it("object with required `undefined`", () => { + const result = makeReadonly(objectWithUndefined); + assertIdenticalTypes(result, createInstanceOf<{ readonly undef: undefined }>()); + }); + it("object with optional `undefined`", () => { + const result = makeReadonly(objectWithOptionalUndefined); + assertIdenticalTypes(result, createInstanceOf<{ readonly optUndef?: undefined }>()); + }); + it("object with optional `bigint`", () => { + const result = makeReadonly(objectWithOptionalBigint); + assertIdenticalTypes(result, createInstanceOf<{ readonly bigint?: bigint }>()); + }); + it("object with optional `unknown`", () => { + const result = makeReadonly(objectWithOptionalUnknown); + assertIdenticalTypes(result, createInstanceOf<{ readonly optUnknown?: unknown }>()); + }); + it("object with exactly `never`", () => { + const result = makeReadonly(objectWithNever); + assertIdenticalTypes(result, createInstanceOf<{ readonly never: never }>()); + }); + it("object with `number | undefined`", () => { + const result = makeReadonly(objectWithNumberOrUndefinedUndefined); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly numOrUndef: number | undefined; + }>(), + ); + }); + it("object with `string | symbol`", () => { + const result = makeReadonly(objectWithStringOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly stringOrSymbol: string | symbol }>(), + ); + }); + it("object with `bigint | string`", () => { + const result = makeReadonly(objectWithBigintOrString); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly bigintOrString: bigint | string }>(), + ); + }); + it("object with `bigint | symbol`", () => { + const result = makeReadonly(objectWithBigintOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly bigintOrSymbol: bigint | symbol }>(), + ); + }); + it("object with `Function | symbol`", () => { + const result = makeReadonly(objectWithFunctionOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly functionOrSymbol: (() => void) | symbol }>(), + ); + }); + it("object with `number | bigint | symbol`", () => { + const result = makeReadonly(objectWithNumberOrBigintOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly numberOrBigintOrSymbol: number | bigint | symbol }>(), + ); + }); + + it("object with function with properties", () => { + const result = makeReadonly(objectWithFunctionWithProperties); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly function: (() => number) & { + readonly property: number; + }; + }>(), + ); + }); + it("object with object and function", () => { + const result = makeReadonly(objectWithObjectAndFunction); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly object: (() => number) & { + readonly property: number; + }; + }>(), + ); + }); + + it("object with number key", () => { + const result = makeReadonly(objectWithNumberKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly 3: string }>()); + }); + it("object with symbol key", () => { + const result = makeReadonly(objectWithSymbolKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly [x: symbol]: string }>()); + }); + it("object with unique symbol key", () => { + const result = makeReadonly(objectWithUniqueSymbolKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly [uniqueSymbol]: string }>()); + }); + + it("object with array of `number`s", () => { + const result = makeReadonly(objectWithArrayOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfNumbers: readonly number[] }>(), + ); + }); + it("object with array of `number | undefined`", () => { + const result = makeReadonly(objectWithArrayOfNumbersOrUndefined); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfNumbersOrUndefined: readonly (number | undefined)[]; + }>(), + ); + }); + it("object with array of `bigint`s", () => { + const result = makeReadonly(objectWithArrayOfBigints); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfBigints: readonly bigint[] }>(), + ); + }); + it("object with array of `symbol`s", () => { + const result = makeReadonly(objectWithArrayOfSymbols); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfSymbols: readonly symbol[] }>(), + ); + }); + it("object with array of `unknown`", () => { + const result = makeReadonly(objectWithArrayOfUnknown); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfUnknown: readonly unknown[] }>(), + ); + }); + it("object with array of functions", () => { + const result = makeReadonly(objectWithArrayOfFunctions); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfFunctions: readonly (typeof aFunction)[] }>(), + ); + }); + it("object with array of functions with properties", () => { + const result = makeReadonly(objectWithArrayOfFunctionsWithProperties); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfFunctionsWithProperties: readonly ((() => number) & { + readonly property: number; + })[]; + }>(), + ); + }); + it("object with array of objects and functions", () => { + const result = makeReadonly(objectWithArrayOfObjectAndFunctions); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfObjectAndFunctions: readonly ((() => number) & { + readonly property: number; + })[]; + }>(), + ); + }); + it("object with array of `bigint`s or objects", () => { + const result = makeReadonly(objectWithArrayOfBigintOrObjects); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfBigintOrObjects: readonly ( + | bigint + | { + readonly property: string; + } + )[]; + }>(), + ); + }); + it("object with array of `symbol`s or objects", () => { + const result = makeReadonly(objectWithArrayOfSymbolOrObjects); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfSymbolOrObjects: readonly ( + | symbol + | { + readonly property: string; + } + )[]; + }>(), + ); + }); + it("object with readonly array of `number`", () => { + const result = makeReadonly(objectWithReadonlyArrayOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly readonlyArrayOfNumbers: readonly number[] }>(), + ); + }); + + it("`string` indexed record of `number`s", () => { + const result = makeReadonly(stringRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly [x: string]: number; + }>(), + ); + }); + it("`string` indexed record of `unknown`s", () => { + const result = makeReadonly(stringRecordOfUnknown); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly [x: string]: unknown; + }>(), + ); + }); + it("`string`|`number` indexed record of `string`s", () => { + const result = makeReadonly(stringOrNumberRecordOfStrings); + assertIdenticalTypes( + result, + createInstanceOf>>(), + ); + }); + it("`string`|`number` indexed record of objects", () => { + const result = makeReadonly(stringOrNumberRecordOfObjects); + assertIdenticalTypes( + result, + createInstanceOf>>(), + ); + }); + it("`string` indexed record of `number`|`string`s with known properties", () => { + const result = makeReadonly(stringRecordOfNumbersOrStringsWithKnownProperties); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`string` indexed record of `unknown` and optional known properties", () => { + const result = makeReadonly(stringRecordOfUnknownWithOptionalKnownProperties); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`string`|`number` indexed record of `strings` with known `number` property (unassignable)", () => { + const result = makeReadonly(stringOrNumberRecordOfStringWithKnownNumber); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`Partial<>` `string` indexed record of `numbers`", () => { + // Warning: as of TypeScript 5.8.2, a Partial<> of an indexed type + // gains `| undefined` even under exactOptionalPropertyTypes=true. + // Preferred result is that there is no change applying Partial<>. + // In either case, this test can hold since there isn't a downside + // to allowing `undefined` in the result if that is how type is + // given since indexed properties are always inherently optional. + const result = makeReadonly(partialStringRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`Partial<>` `string` indexed record of `numbers`", () => { + const result = makeReadonly(partialStringRecordOfUnknown); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("templated record of `numbers`", () => { + const result = makeReadonly(templatedRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("templated record of `numbers`", () => { + const result = makeReadonly(mixedRecordOfUnknown); + assertIdenticalTypes(result, createInstanceOf>()); + }); + + it("object with recursion and `symbol`", () => { + const result = makeReadonly(objectWithSymbolOrRecursion); + assertIdenticalTypes(result, readonlyObjectWithSymbolOrRecursion); + }); + + it("object with recursion and handle", () => { + const result = makeReadonly(objectWithFluidHandleOrRecursion); + assertIdenticalTypes(result, readonlyObjectWithFluidHandleOrRecursion); + }); + + it("object with function object with recursion", () => { + const objectWithSelfRecursiveFunctionWithProperties = { + outerFnOjb: selfRecursiveFunctionWithProperties, + }; + const result = makeReadonly(objectWithSelfRecursiveFunctionWithProperties); + const expected = { + outerFnOjb: readonlySelfRecursiveFunctionWithProperties, + } as const; + assertIdenticalTypes(result, expected); + }); + it("object with object and function with recursion", () => { + const objectWithSelfRecursiveObjectAndFunction = { + outerFnOjb: selfRecursiveObjectAndFunction, + }; + const result = makeReadonly(objectWithSelfRecursiveObjectAndFunction); + const expected = { + outerFnOjb: readonlySelfRecursiveObjectAndFunction, + } as const; + assertIdenticalTypes(result, expected); + }); + + it("object with possible type recursion through union", () => { + const result = makeReadonly(objectWithPossibleRecursion); + interface ReadonlyObjectWithPossibleRecursion { + readonly [x: string]: ReadonlyObjectWithPossibleRecursion | string; + } + assertIdenticalTypes(result, createInstanceOf()); + }); + it("object with optional type recursion", () => { + const result = makeReadonly(objectWithOptionalRecursion); + assertIdenticalTypes(result, readonlyObjectWithOptionalRecursion); + }); + it("object with deep type recursion", () => { + const result = makeReadonly(objectWithEmbeddedRecursion); + assertIdenticalTypes(result, readonlyObjectWithEmbeddedRecursion); + }); + it("object with alternating type recursion", () => { + const result = makeReadonly(objectWithAlternatingRecursion); + assertIdenticalTypes(result, readonlyObjectWithAlternatingRecursion); + }); + + it("object with inherited recursion and extended with mutable properties", () => { + const result = makeReadonly({ + outer: objectInheritingOptionalRecursionAndWithNestedSymbol, + }); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly outer: { + readonly recursive?: ReadonlyObjectWithOptionalRecursion; + readonly complex: { readonly number: number; readonly symbol: symbol }; + }; + }>(), + ); + }); + + it("`string` indexed record of recursion or `number`", () => { + const result = makeReadonly(stringRecordWithRecursionOrNumber); + assertIdenticalTypes(result, readonlyStringRecordWithRecursionOrNumber); + }); + + it("simple json (`JsonTypeWith`)", () => { + const result = makeReadonly(simpleJson); + assertIdenticalTypes(result, simpleImmutableJson); + }); + + it("simple non-null object json (`NonNullJsonObjectWith`)", () => { + const result = makeReadonly(jsonObject); + assertIdenticalTypes(result, immutableJsonObject); + }); + + it("non-const enum", () => { + // Note: typescript doesn't do a great job checking that a generated type satisfies an enum + // type. The numeric indices are not checked. So far, most robust inspection is manual + // after any change. + const resultNumericRead = makeReadonly(NumericEnum); + assertIdenticalTypes(resultNumericRead, NumericEnum); + const resultStringRead = makeReadonly(StringEnum); + assertIdenticalTypes(resultStringRead, StringEnum); + const resultComputedRead = makeReadonly(ComputedEnum); + assertIdenticalTypes(resultComputedRead, ComputedEnum); + }); + + it("object with matched getter and setter", () => { + const result = makeReadonly(objectWithMatchedGetterAndSetterProperty); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + }); + + it("object with matched getter and setter implemented via value", () => { + const result = makeReadonly(objectWithMatchedGetterAndSetterPropertyViaValue); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + }); + it("object with mismatched getter and setter implemented via value", () => { + const result = makeReadonly(objectWithMismatchedGetterAndSetterPropertyViaValue); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + // @ts-expect-error Cannot assign to 'property' because it is a read-only property + result.property = -1; + }); + it("object with mismatched getter and setter", () => { + const result = makeReadonly(objectWithMismatchedGetterAndSetterProperty); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + + assert.throws(() => { + // @ts-expect-error Cannot assign to 'property' because it is a read-only property + result.property = -1; + }, new Error( + "ClassImplementsObjectWithMismatchedGetterAndSetterProperty writing 'property' as -1", + )); + }); + + describe("class instance", () => { + it("with public data", () => { + const result = makeReadonly(classInstanceWithPublicData); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("with public method", () => { + const result = makeReadonly(classInstanceWithPublicMethod); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + result satisfies typeof classInstanceWithPublicMethod; + }); + it("with public data and is function", () => { + const result = makeReadonly(classInstanceWithPublicDataAndIsFunction); + assertIdenticalTypes( + result, + createInstanceOf<(() => 26) & { readonly public: string }>(), + ); + }); + }); + + it("object with optional property (remains optional)", () => { + const result = makeReadonly(objectWithOptionalNumberNotPresent); + assertIdenticalTypes(result, createInstanceOf<{ readonly optNumber?: number }>()); + }); + }); + + describe("function & object intersections result in immutable object portion", () => { + it("function with properties", () => { + const result = makeReadonly(functionWithProperties); + assertIdenticalTypes( + result, + createInstanceOf< + (() => number) & { + readonly property: number; + } + >(), + ); + }); + it("object and function", () => { + const result = makeReadonly(objectAndFunction); + assertIdenticalTypes( + result, + createInstanceOf< + { + readonly property: number; + } & (() => number) + >(), + ); + }); + it("function with class instance with private data", () => { + const result = makeReadonly(functionObjectWithPrivateData); + assertIdenticalTypes( + result, + createInstanceOf< + (() => 23) & { + readonly public: string; + } + >(), + ); + }); + it("function with class instance with public data", () => { + const result = makeReadonly(functionObjectWithPublicData); + assertIdenticalTypes( + result, + createInstanceOf< + (() => 24) & { + readonly public: string; + } + >(), + ); + }); + it("function object with recursion", () => { + const result = makeReadonly(selfRecursiveFunctionWithProperties); + assertIdenticalTypes(result, readonlySelfRecursiveFunctionWithProperties); + }); + it("object and function with recursion", () => { + const result = makeReadonly(selfRecursiveObjectAndFunction); + assertIdenticalTypes(result, readonlySelfRecursiveObjectAndFunction); + }); + }); + + describe("read-only objects are preserved", () => { + it("object with `readonly`", () => { + const result = makeReadonly(objectWithReadonly); + assertIdenticalTypes(result, objectWithReadonly); + }); + + it("object with getter implemented via value", () => { + const result = makeReadonly(objectWithGetterViaValue); + assertIdenticalTypes(result, objectWithGetterViaValue); + }); + it("object with `readonly` implemented via getter", () => { + const result = makeReadonly(objectWithReadonlyViaGetter); + assertIdenticalTypes(result, objectWithReadonlyViaGetter); + }); + + it("object with getter", () => { + const result = makeReadonly(objectWithGetter); + assertIdenticalTypes(result, objectWithGetter); + + assert.throws(() => { + // @ts-expect-error Cannot assign to 'getter' because it is a read-only property. + objectWithGetter.getter = -1; + }, new TypeError( + "Cannot set property getter of # which has only a getter", + )); + assert.throws(() => { + // @ts-expect-error Cannot assign to 'getter' because it is a read-only property. + result.getter = -1; + }, new TypeError( + "Cannot set property getter of # which has only a getter", + )); + }); + + it("simple read-only json (`ReadonlyJsonTypeWith`)", () => { + const result = makeReadonly(simpleImmutableJson); + assertIdenticalTypes(result, simpleImmutableJson); + }); + + it("simple read-only non-null object json (`NonNullJsonObjectWith`)", () => { + const result = makeReadonly(immutableJsonObject); + assertIdenticalTypes(result, immutableJsonObject); + }); + }); + + describe("built-in or common class instances", () => { + it("ErasedType is preserved", () => { + const result = makeReadonly(erasedType); + assertIdenticalTypes(result, erasedType); + }); + + describe("`IFluidHandle` becomes `Readonly>`", () => { + it("`IFluidHandle`", () => { + const result = makeReadonly(fluidHandleToNumber); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("`IFluidHandle<{...}>` generic remains intact by default", () => { + const result = makeReadonly(fluidHandleToRecord); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("`IFluidHandle<{...}>` generic becomes deeply immutable when enabled", () => { + const result = makeReadonlyDeepeningHandleTypes(fluidHandleToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + Readonly< + IFluidHandle<{ + readonly [p: string]: { readonly x: number; readonly y: number }; + }> + > + >(), + ); + }); + it("object with `IFluidHandle`", () => { + const result = makeReadonly(objectWithFluidHandle); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly handle: Readonly>; + }>(), + ); + }); + it("object with `IFluidHandle` or recursion", () => { + const result = makeReadonly(objectWithFluidHandleOrRecursion); + assertIdenticalTypes(result, readonlyObjectWithFluidHandleOrRecursion); + }); + it("read-only object with `FluidHandle` or recursion", () => { + const result = makeReadonly(readonlyObjectWithFluidHandleOrRecursion); + assertIdenticalTypes(result, readonlyObjectWithFluidHandleOrRecursion); + }); + }); + + describe("Branded primitive is preserved", () => { + it("`number & BrandedType`", () => { + const result = makeReadonly(brandedNumber); + assertIdenticalTypes(result, brandedNumber); + }); + it("`string & BrandedType`", () => { + const result = makeReadonly(brandedString); + assertIdenticalTypes(result, brandedString); + }); + it("object with `number & BrandedType`", () => { + const result = makeReadonly(objectWithBrandedNumber); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("object with `string & BrandedType`", () => { + const result = makeReadonly(objectWithBrandedString); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + }); + + describe("that are mutable become immutable version", () => { + describe("Map becomes ReadonlyMap", () => { + it("Map", () => { + const result = makeReadonly(mapOfStringsToNumbers); + assertIdenticalTypes(result, readonlyMapOfStringsToNumbers); + + mapOfStringsToNumbers satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof mapOfStringsToNumbers; + }); + it("Map generics become deeply immutable by default", () => { + const result = makeReadonly(mapOfPointToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlyMap< + Readonly, + { + readonly [p: string]: Readonly; + } + > + >(), + ); + + mapOfPointToRecord satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof mapOfPointToRecord; + }); + it("Map generics are intact when built-in deepening is disabled", () => { + const result = makeReadonlyNoGenericsDeepening(mapOfPointToRecord); + assertIdenticalTypes(result, readonlyMapOfPointToRecord); + }); + }); + describe("Set becomes ReadonlySet", () => { + it("Set", () => { + const result = makeReadonly(setOfNumbers); + assertIdenticalTypes(result, readonlySetOfNumbers); + + setOfNumbers satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof setOfNumbers; + }); + it("Set generics become deeply immutable by default", () => { + const result = makeReadonly(setOfRecords); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlySet<{ + readonly [p: string]: Readonly; + }> + >(), + ); + + setOfRecords satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof setOfRecords; + }); + it("Set generics are intact when built-in deepening is disabled", () => { + const result = makeReadonlyNoGenericsDeepening(setOfRecords); + assertIdenticalTypes(result, readonlySetOfRecords); + }); + }); + }); + describe("that are immutable make generics immutable by default", () => { + it("ReadonlyMap", () => { + const result = makeReadonly(readonlyMapOfStringsToNumbers); + assertIdenticalTypes(result, readonlyMapOfStringsToNumbers); + result satisfies typeof readonlyMapOfStringsToNumbers; + }); + it("ReadonlyMap", () => { + const result = makeReadonly(readonlyMapOfPointToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlyMap< + Readonly, + { + readonly [p: string]: Readonly; + } + > + >(), + ); + result satisfies typeof readonlyMapOfPointToRecord; + }); + it("ReadonlySet", () => { + const result = makeReadonly(readonlySetOfNumbers); + assertIdenticalTypes(result, readonlySetOfNumbers); + result satisfies typeof readonlySetOfNumbers; + }); + it("ReadonlySet", () => { + const result = makeReadonly(readonlySetOfRecords); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlySet<{ + readonly [p: string]: Readonly; + }> + >(), + ); + result satisfies typeof readonlySetOfRecords; + }); + }); + describe("that are immutable keep generics intact when deepening disabled", () => { + it("ReadonlyMap", () => { + const result = makeReadonlyNoGenericsDeepening(readonlyMapOfPointToRecord); + assertIdenticalTypes(result, readonlyMapOfPointToRecord); + result satisfies typeof readonlyMapOfPointToRecord; + }); + it("ReadonlySet", () => { + const result = makeReadonlyNoGenericsDeepening(readonlySetOfRecords); + assertIdenticalTypes(result, readonlySetOfRecords); + result satisfies typeof readonlySetOfRecords; + }); + }); + }); + + describe("partially supported object types are modified", () => { + describe("class instance non-public properties are removed", () => { + it("with private method", () => { + const result = makeReadonly(classInstanceWithPrivateMethod); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error getSecret is missing, but required + result satisfies typeof classInstanceWithPrivateMethod; + // @ts-expect-error getSecret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateMethod); + }); + it("with private getter", () => { + const result = makeReadonly(classInstanceWithPrivateGetter); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateGetter; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateGetter); + }); + it("with private setter", () => { + const result = makeReadonly(classInstanceWithPrivateSetter); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateSetter; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateSetter); + }); + it("with private data", () => { + const result = makeReadonly(classInstanceWithPrivateData); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateData; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateData); + }); + it("with private data and is function", () => { + const result = makeReadonly(classInstanceWithPrivateDataAndIsFunction); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly public: string } & (() => 25)>(), + ); + }); + }); + + // A class branded object cannot be detected without knowing the branding type. + // And will class privates removed just the original type is made immutable. + describe("branded non-primitive types lose branding", () => { + // class branding with `object` is just the class branding and produces + // an empty object, {}, which happens to be a special any object type. + it("`object & BrandedType`", () => { + const result = makeReadonly(brandedObject); + assertIdenticalTypes(result, {}); + }); + it("`object & BrandedType`", () => { + const result = makeReadonly(brandedObjectWithString); + assertIdenticalTypes(result, createInstanceOf<{ readonly string: string }>()); + }); + }); + }); + + describe("types that cannot be made immutable are unchanged", () => { + it("`any`", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const any: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result = makeReadonly(any); + assertIdenticalTypes(result, any); + }); + it("`unknown`", () => { + const result = makeReadonly(unknownValueOfSimpleRecord); + assertIdenticalTypes(result, unknownValueOfSimpleRecord); + }); + it("`object` (plain object)", () => { + const result = makeReadonly(object); + assertIdenticalTypes(result, object); + }); + it("`null`", () => { + /* eslint-disable unicorn/no-null */ + const result = makeReadonly(null); + assertIdenticalTypes(result, null); + /* eslint-enable unicorn/no-null */ + }); + it("`void`", () => { + const result = makeReadonly(voidValue); + assertIdenticalTypes(result, voidValue); + }); + it("`never`", () => { + const result = makeReadonly(never); + assertIdenticalTypes(result, never); + }); + }); + + describe("using `RecurseLimit` limits processing of recursive types", () => { + it("no recursion: object with optional type recursion is readonly once", () => { + const result = makeReadonlyBailingOnRecursiveTypes(objectWithOptionalRecursion); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly recursive?: typeof objectWithOptionalRecursion; + }>(), + ); + }); + it("3 recursions: object with optional type recursion is readonly thru 4 levels", () => { + const result = makeReadonlyWithRecurseLimitThree(objectWithOptionalRecursion); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly recursive?: { + readonly recursive?: { + readonly recursive?: { + readonly recursive?: typeof objectWithOptionalRecursion; + }; + }; + }; + }>(), + ); + }); + }); + + describe("unsupported types", () => { + // These cases are demonstrating defects within the current implementation. + // They show "allowed" incorrect use and the unexpected results. + describe("known defect expectations", () => { + it("object with setter becomes read-only property", () => { + const result = makeReadonly(objectWithSetter); + // @ts-expect-error `setter` is no longer mutable + assertIdenticalTypes(result, objectWithSetter); + assertIdenticalTypes(result, createInstanceOf<{ readonly setter: string }>()); + + // Read from setter only produces `undefined` but is typed as `string`. + const originalSetterValue = objectWithSetter.setter; + assert.equal(originalSetterValue, undefined); + // Read from modified type is the same (as it is the same object). + const resultSetterValue = result.setter; + assert.equal(resultSetterValue, undefined); + + assert.throws(() => { + objectWithSetter.setter = "test string 1"; + }, new Error("ClassImplementsObjectWithSetter writing 'setter' as test string 1")); + assert.throws(() => { + // @ts-expect-error Cannot assign to 'setter' because it is a read-only property. + result.setter = "test string 2"; + }, new Error("ClassImplementsObjectWithSetter writing 'setter' as test string 2")); + }); + }); + + it("unique symbol becomes `symbol`", () => { + const result = makeReadonly(uniqueSymbol); + // @ts-expect-error `uniqueSymbol` is no longer a specific (unique) symbol + assertIdenticalTypes(result, uniqueSymbol); + // mysteriously becomes `symbol` (can't be preserved) + assertIdenticalTypes(result, symbol); + }); + }); +}); diff --git a/packages/common/core-interfaces/src/test/jsonDeserialized.spec.ts b/packages/common/core-interfaces/src/test/jsonDeserialized.spec.ts index c5e716f9904b..9dde60e18637 100644 --- a/packages/common/core-interfaces/src/test/jsonDeserialized.spec.ts +++ b/packages/common/core-interfaces/src/test/jsonDeserialized.spec.ts @@ -90,7 +90,7 @@ import { objectWithArrayOfFunctionsWithProperties, objectWithArrayOfObjectAndFunctions, objectWithArrayOfBigintOrObjects, - objectWithArrayOfSymbolsOrObjects, + objectWithArrayOfSymbolOrObjects, objectWithReadonlyArrayOfNumbers, objectWithOptionalNumberNotPresent, objectWithOptionalNumberUndefined, @@ -112,6 +112,7 @@ import { stringRecordOfUndefined, stringRecordOfUnknown, stringOrNumberRecordOfStrings, + stringOrNumberRecordOfObjects, partialStringRecordOfNumbers, partialStringRecordOfUnknown, templatedRecordOfNumbers, @@ -140,7 +141,9 @@ import { selfRecursiveObjectAndFunction, objectInheritingOptionalRecursionAndWithNestedSymbol, simpleJson, + simpleImmutableJson, jsonObject, + immutableJsonObject, classInstanceWithPrivateData, classInstanceWithPrivateMethod, classInstanceWithPrivateGetter, @@ -522,6 +525,10 @@ describe("JsonDeserialized", () => { const resultRead = passThru(stringOrNumberRecordOfStrings); assertIdenticalTypes(resultRead, stringOrNumberRecordOfStrings); }); + it("`string`|`number` indexed record of objects", () => { + const resultRead = passThru(stringOrNumberRecordOfObjects); + assertIdenticalTypes(resultRead, stringOrNumberRecordOfObjects); + }); it("`string` indexed record of `number`|`string`s with known properties", () => { const resultRead = passThru(stringRecordOfNumbersOrStringsWithKnownProperties); assertIdenticalTypes(resultRead, stringRecordOfNumbersOrStringsWithKnownProperties); @@ -582,6 +589,10 @@ describe("JsonDeserialized", () => { const resultRead = passThru(jsonObject); assertIdenticalTypes(resultRead, jsonObject); }); + it("simple read-only non-null object json (`ReadonlyNonNullJsonObjectWith`)", () => { + const resultRead = passThru(immutableJsonObject); + assertIdenticalTypes(resultRead, immutableJsonObject); + }); it("non-const enum", () => { // Note: typescript doesn't do a great job checking that a filtered type satisfies an enum @@ -956,7 +967,7 @@ describe("JsonDeserialized", () => { ); }); it("object with array of partially supported (symbols or basic object) is modified with null", () => { - const resultRead = passThru(objectWithArrayOfSymbolsOrObjects, { + const resultRead = passThru(objectWithArrayOfSymbolOrObjects, { arrayOfSymbolOrObjects: [null], }); assertIdenticalTypes( @@ -1372,6 +1383,10 @@ describe("JsonDeserialized", () => { const resultRead = passThru(simpleJson); assertIdenticalTypes(resultRead, simpleJson); }); + it("simple read-only json (`ReadonlyJsonTypeWith`)", () => { + const resultRead = passThru(simpleImmutableJson); + assertIdenticalTypes(resultRead, simpleImmutableJson); + }); }); describe("unsupported object types", () => { diff --git a/packages/common/core-interfaces/src/test/jsonSerializable.spec.ts b/packages/common/core-interfaces/src/test/jsonSerializable.spec.ts index 36f9403d2f49..11fe2d955184 100644 --- a/packages/common/core-interfaces/src/test/jsonSerializable.spec.ts +++ b/packages/common/core-interfaces/src/test/jsonSerializable.spec.ts @@ -93,7 +93,7 @@ import { objectWithArrayOfFunctionsWithProperties, objectWithArrayOfObjectAndFunctions, objectWithArrayOfBigintOrObjects, - objectWithArrayOfSymbolsOrObjects, + objectWithArrayOfSymbolOrObjects, objectWithReadonlyArrayOfNumbers, objectWithOptionalNumberNotPresent, objectWithOptionalNumberUndefined, @@ -116,6 +116,7 @@ import { stringRecordOfUndefined, stringRecordOfUnknown, stringOrNumberRecordOfStrings, + stringOrNumberRecordOfObjects, partialStringRecordOfNumbers, partialStringRecordOfUnknown, templatedRecordOfNumbers, @@ -144,7 +145,9 @@ import { selfRecursiveObjectAndFunction, objectInheritingOptionalRecursionAndWithNestedSymbol, simpleJson, + simpleImmutableJson, jsonObject, + immutableJsonObject, classInstanceWithPrivateData, classInstanceWithPrivateMethod, classInstanceWithPrivateGetter, @@ -512,6 +515,10 @@ describe("JsonSerializable", () => { const { filteredIn } = passThru(stringOrNumberRecordOfStrings); assertIdenticalTypes(filteredIn, stringOrNumberRecordOfStrings); }); + it("`string`|`number` indexed record of objects", () => { + const { filteredIn } = passThru(stringOrNumberRecordOfObjects); + assertIdenticalTypes(filteredIn, stringOrNumberRecordOfObjects); + }); it("templated record of `numbers`", () => { const { filteredIn } = passThru(templatedRecordOfNumbers); assertIdenticalTypes(templatedRecordOfNumbers, filteredIn); @@ -547,6 +554,10 @@ describe("JsonSerializable", () => { const { filteredIn } = passThru(jsonObject); assertIdenticalTypes(filteredIn, jsonObject); }); + it("simple read-only non-null object json (ReadonlyNonNullJsonObjectWith)", () => { + const { filteredIn } = passThru(immutableJsonObject); + assertIdenticalTypes(filteredIn, immutableJsonObject); + }); it("non-const enums", () => { // Note: typescript doesn't do a great job checking that a filtered type satisfies an enum @@ -662,6 +673,10 @@ describe("JsonSerializable", () => { const { filteredIn } = passThru(simpleJson); assertIdenticalTypes(filteredIn, simpleJson); }); + it("simple read-only json (ReadonlyJsonTypeWith)", () => { + const { filteredIn } = passThru(simpleImmutableJson); + assertIdenticalTypes(filteredIn, simpleImmutableJson); + }); }); describe("unsupported object types", () => { @@ -1259,7 +1274,7 @@ describe("JsonSerializable", () => { it("object with array of `symbol` or basic object", () => { const { filteredIn } = passThru( // @ts-expect-error 'symbol' is not supported (becomes 'never') - objectWithArrayOfSymbolsOrObjects, + objectWithArrayOfSymbolOrObjects, { arrayOfSymbolOrObjects: [null] }, ); assertIdenticalTypes( diff --git a/packages/common/core-interfaces/src/test/shallowReadonly.spec.ts b/packages/common/core-interfaces/src/test/shallowReadonly.spec.ts new file mode 100644 index 000000000000..96a02e720fd5 --- /dev/null +++ b/packages/common/core-interfaces/src/test/shallowReadonly.spec.ts @@ -0,0 +1,1313 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { assertIdenticalTypes, createInstanceOf } from "./testUtils.js"; +import type { + ClassWithPublicData, + Point, + SelfRecursiveFunctionWithProperties, + SelfRecursiveObjectAndFunction, +} from "./testValues.js"; +import { + boolean, + number, + string, + numericEnumValue, + NumericEnum, + stringEnumValue, + StringEnum, + constHeterogenousEnumValue, + ConstHeterogenousEnum, + computedEnumValue, + ComputedEnum, + objectWithLiterals, + arrayOfLiterals, + tupleWithLiterals, + symbol, + uniqueSymbol, + bigint, + aFunction, + unknownValueOfSimpleRecord, + voidValue, + never, + stringOrSymbol, + bigintOrString, + bigintOrSymbol, + numberOrBigintOrSymbol, + functionWithProperties, + objectAndFunction, + arrayOfNumbers, + arrayOfNumbersSparse, + arrayOfNumbersOrUndefined, + arrayOfBigints, + arrayOfSymbols, + arrayOfFunctions, + arrayOfFunctionsWithProperties, + arrayOfObjectAndFunctions, + arrayOfBigintOrSymbols, + arrayOfNumberBigintOrSymbols, + arrayOfBigintOrObjects, + arrayOfSymbolOrObjects, + readonlyArrayOfNumbers, + readonlyArrayOfObjects, + object, + emptyObject, + objectWithBoolean, + objectWithNumber, + objectWithString, + objectWithSymbol, + objectWithBigint, + objectWithFunction, + objectWithFunctionWithProperties, + objectWithObjectAndFunction, + objectWithBigintOrString, + objectWithBigintOrSymbol, + objectWithNumberOrBigintOrSymbol, + objectWithFunctionOrSymbol, + objectWithStringOrSymbol, + objectWithUnknown, + objectWithOptionalUnknown, + objectWithUndefined, + objectWithOptionalUndefined, + objectWithOptionalBigint, + objectWithNumberKey, + objectWithSymbolKey, + objectWithUniqueSymbolKey, + objectWithArrayOfNumbers, + objectWithArrayOfNumbersOrUndefined, + objectWithArrayOfBigints, + objectWithArrayOfSymbols, + objectWithArrayOfUnknown, + objectWithArrayOfFunctions, + objectWithArrayOfFunctionsWithProperties, + objectWithArrayOfObjectAndFunctions, + objectWithArrayOfBigintOrObjects, + objectWithArrayOfSymbolOrObjects, + objectWithReadonlyArrayOfNumbers, + objectWithOptionalNumberNotPresent, + objectWithNumberOrUndefinedUndefined, + objectWithReadonly, + objectWithReadonlyViaGetter, + objectWithGetter, + objectWithGetterViaValue, + objectWithSetter, + objectWithMatchedGetterAndSetterProperty, + objectWithMatchedGetterAndSetterPropertyViaValue, + objectWithMismatchedGetterAndSetterProperty, + objectWithMismatchedGetterAndSetterPropertyViaValue, + objectWithNever, + stringRecordOfNumbers, + stringRecordOfUnknown, + stringOrNumberRecordOfStrings, + stringOrNumberRecordOfObjects, + partialStringRecordOfNumbers, + partialStringRecordOfUnknown, + templatedRecordOfNumbers, + mixedRecordOfUnknown, + stringRecordOfNumbersOrStringsWithKnownProperties, + stringRecordOfUnknownWithOptionalKnownProperties, + stringOrNumberRecordOfStringWithKnownNumber, + objectWithPossibleRecursion, + objectWithOptionalRecursion, + objectWithEmbeddedRecursion, + objectWithAlternatingRecursion, + objectWithSymbolOrRecursion, + objectWithFluidHandleOrRecursion, + stringRecordWithRecursionOrNumber, + selfRecursiveFunctionWithProperties, + selfRecursiveObjectAndFunction, + objectInheritingOptionalRecursionAndWithNestedSymbol, + simpleJson, + simpleImmutableJson, + jsonObject, + immutableJsonObject, + classInstanceWithPrivateData, + classInstanceWithPrivateMethod, + classInstanceWithPrivateGetter, + classInstanceWithPrivateSetter, + classInstanceWithPublicData, + classInstanceWithPublicMethod, + functionObjectWithPrivateData, + functionObjectWithPublicData, + classInstanceWithPrivateDataAndIsFunction, + classInstanceWithPublicDataAndIsFunction, + mapOfStringsToNumbers, + readonlyMapOfStringsToNumbers, + mapOfPointToRecord, + readonlyMapOfPointToRecord, + setOfNumbers, + readonlySetOfNumbers, + setOfRecords, + readonlySetOfRecords, + brandedNumber, + brandedString, + brandedObject, + brandedObjectWithString, + objectWithBrandedNumber, + objectWithBrandedString, + fluidHandleToNumber, + fluidHandleToRecord, + objectWithFluidHandle, + readonlyObjectWithFluidHandleOrRecursion, + erasedType, +} from "./testValues.js"; + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import type { + ReadonlySupportedGenerics, + ShallowReadonly, +} from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +/** + * Result is defined using `ShallowReadonly` type generator. + * + * @param v - value whose type is passed through `ShallowReadonly` + * @returns the original value with modified type + */ +function makeReadonly(v: T) { + return v as ShallowReadonly; +} + +function makeReadonlyDeepeningAllSupportedGenerics(v: T) { + return v as ShallowReadonly; +} + +function makeReadonlyNoGenericsDeepening(v: T) { + return v as ShallowReadonly; +} + +/* eslint-enable @typescript-eslint/explicit-function-return-type */ + +describe("ShallowReadonly", () => { + describe("primitive types are preserved", () => { + it("`undefined`", () => { + const result = makeReadonly(undefined); + assertIdenticalTypes(result, undefined); + }); + it("`boolean`", () => { + const result = makeReadonly(boolean); + assertIdenticalTypes(result, boolean); + }); + it("`number`", () => { + const result = makeReadonly(number); + assertIdenticalTypes(result, number); + }); + it("`string`", () => { + const result = makeReadonly(string); + assertIdenticalTypes(result, string); + }); + it("`symbol`", () => { + const result = makeReadonly(symbol); + assertIdenticalTypes(result, symbol); + }); + it("`bigint`", () => { + const result = makeReadonly(bigint); + assertIdenticalTypes(result, bigint); + }); + it("function", () => { + const result = makeReadonly(aFunction); + assertIdenticalTypes(result, aFunction); + }); + it("numeric enum", () => { + const result = makeReadonly(numericEnumValue); + assertIdenticalTypes(result, numericEnumValue); + }); + it("string enum", () => { + const result = makeReadonly(stringEnumValue); + assertIdenticalTypes(result, stringEnumValue); + }); + it("const heterogenous enum", () => { + const result = makeReadonly(constHeterogenousEnumValue); + assertIdenticalTypes(result, constHeterogenousEnumValue); + }); + it("computed enum", () => { + const result = makeReadonly(computedEnumValue); + assertIdenticalTypes(result, computedEnumValue); + }); + }); + + describe("unions of primitive types are preserved", () => { + it("`string | symbol`", () => { + const result = makeReadonly(stringOrSymbol); + assertIdenticalTypes(result, stringOrSymbol); + }); + it("`bigint | string`", () => { + const result = makeReadonly(bigintOrString); + assertIdenticalTypes(result, bigintOrString); + }); + it("`bigint | symbol`", () => { + const result = makeReadonly(bigintOrSymbol); + assertIdenticalTypes(result, bigintOrSymbol); + }); + it("`number | bigint | symbol`", () => { + const result = makeReadonly(numberOrBigintOrSymbol); + assertIdenticalTypes(result, numberOrBigintOrSymbol); + }); + }); + + describe("literal types are preserved", () => { + it("`true`", () => { + const result = makeReadonly(true as const); + assertIdenticalTypes(result, true); + }); + it("`false`", () => { + const result = makeReadonly(false as const); + assertIdenticalTypes(result, false); + }); + it("`0`", () => { + const result = makeReadonly(0 as const); + assertIdenticalTypes(result, 0); + }); + it('"string"', () => { + const result = makeReadonly("string" as const); + assertIdenticalTypes(result, "string"); + }); + it("object with literals", () => { + const result = makeReadonly(objectWithLiterals); + assertIdenticalTypes(result, objectWithLiterals); + }); + it("array of literals", () => { + const result = makeReadonly(arrayOfLiterals); + assertIdenticalTypes(result, arrayOfLiterals); + }); + it("tuple of literals", () => { + const result = makeReadonly(tupleWithLiterals); + assertIdenticalTypes(result, tupleWithLiterals); + }); + it("specific numeric enum value", () => { + const result = makeReadonly(NumericEnum.two as const); + assertIdenticalTypes(result, NumericEnum.two); + }); + it("specific string enum value", () => { + const result = makeReadonly(StringEnum.b as const); + assertIdenticalTypes(result, StringEnum.b); + }); + it("specific const heterogenous enum value", () => { + const result = makeReadonly(ConstHeterogenousEnum.zero as const); + assertIdenticalTypes(result, ConstHeterogenousEnum.zero); + }); + it("specific computed enum value", () => { + const result = makeReadonly(ComputedEnum.computed as const); + assertIdenticalTypes(result, ComputedEnum.computed); + }); + }); + + describe("arrays become immutable", () => { + it("array of numbers", () => { + const result = makeReadonly(arrayOfNumbers); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("array of numbers with holes", () => { + const result = makeReadonly(arrayOfNumbersSparse); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("array of numbers or undefined", () => { + const result = makeReadonly(arrayOfNumbersOrUndefined); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of bigint or basic object", () => { + const result = makeReadonly(arrayOfBigintOrObjects); + assertIdenticalTypes( + result, + createInstanceOf(), + ); + }); + it("array of supported types (symbols or basic object)", () => { + const result = makeReadonly(arrayOfSymbolOrObjects); + assertIdenticalTypes( + result, + createInstanceOf(), + ); + }); + it("array of bigint", () => { + const result = makeReadonly(arrayOfBigints); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of symbols", () => { + const result = makeReadonly(arrayOfSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of functions", () => { + const result = makeReadonly(arrayOfFunctions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assertIdenticalTypes(result, createInstanceOf any)[]>()); + }); + it("array of functions with properties", () => { + const result = makeReadonly(arrayOfFunctionsWithProperties); + assertIdenticalTypes( + result, + createInstanceOf number) & { property: number })[]>(), + ); + }); + it("array of objects and functions", () => { + const result = makeReadonly(arrayOfObjectAndFunctions); + assertIdenticalTypes( + result, + createInstanceOf number))[]>(), + ); + }); + it("array of `bigint | symbol`", () => { + const result = makeReadonly(arrayOfBigintOrSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + it("array of `number | bigint | symbol`", () => { + const result = makeReadonly(arrayOfNumberBigintOrSymbols); + assertIdenticalTypes(result, createInstanceOf()); + }); + }); + + describe("read-only arrays are preserved", () => { + it("readonly array of primitive is preserved", () => { + const result = makeReadonly(readonlyArrayOfNumbers); + assertIdenticalTypes(result, readonlyArrayOfNumbers); + }); + it("readonly array of mutable object is preserved", () => { + const result = makeReadonly(readonlyArrayOfObjects); + assertIdenticalTypes(result, readonlyArrayOfObjects); + }); + }); + + describe("object properties become immutable", () => { + it("empty object", () => { + const result = makeReadonly(emptyObject); + assertIdenticalTypes(result, emptyObject); + }); + + it("object with `boolean`", () => { + const result = makeReadonly(objectWithBoolean); + assertIdenticalTypes(result, createInstanceOf<{ readonly boolean: boolean }>()); + }); + it("object with `number`", () => { + const result = makeReadonly(objectWithNumber); + assertIdenticalTypes(result, createInstanceOf<{ readonly number: number }>()); + }); + it("object with `string`", () => { + const result = makeReadonly(objectWithString); + assertIdenticalTypes(result, createInstanceOf<{ readonly string: string }>()); + }); + it("object with `bigint`", () => { + const result = makeReadonly(objectWithBigint); + assertIdenticalTypes(result, createInstanceOf<{ readonly bigint: bigint }>()); + }); + it("object with `symbol`", () => { + const result = makeReadonly(objectWithSymbol); + assertIdenticalTypes(result, createInstanceOf<{ readonly symbol: symbol }>()); + }); + it("object with function", () => { + const result = makeReadonly(objectWithFunction); + assertIdenticalTypes(result, createInstanceOf<{ readonly function: () => void }>()); + }); + it("object with `unknown`", () => { + const result = makeReadonly(objectWithUnknown); + assertIdenticalTypes(result, createInstanceOf<{ readonly unknown: unknown }>()); + }); + it("object with required `undefined`", () => { + const result = makeReadonly(objectWithUndefined); + assertIdenticalTypes(result, createInstanceOf<{ readonly undef: undefined }>()); + }); + it("object with optional `undefined`", () => { + const result = makeReadonly(objectWithOptionalUndefined); + assertIdenticalTypes(result, createInstanceOf<{ readonly optUndef?: undefined }>()); + }); + it("object with optional `bigint`", () => { + const result = makeReadonly(objectWithOptionalBigint); + assertIdenticalTypes(result, createInstanceOf<{ readonly bigint?: bigint }>()); + }); + it("object with optional `unknown`", () => { + const result = makeReadonly(objectWithOptionalUnknown); + assertIdenticalTypes(result, createInstanceOf<{ readonly optUnknown?: unknown }>()); + }); + it("object with exactly `never`", () => { + const result = makeReadonly(objectWithNever); + assertIdenticalTypes(result, createInstanceOf<{ readonly never: never }>()); + }); + it("object with `number | undefined`", () => { + const result = makeReadonly(objectWithNumberOrUndefinedUndefined); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly numOrUndef: number | undefined; + }>(), + ); + }); + it("object with `string | symbol`", () => { + const result = makeReadonly(objectWithStringOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly stringOrSymbol: string | symbol }>(), + ); + }); + it("object with `bigint | string`", () => { + const result = makeReadonly(objectWithBigintOrString); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly bigintOrString: bigint | string }>(), + ); + }); + it("object with `bigint | symbol`", () => { + const result = makeReadonly(objectWithBigintOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly bigintOrSymbol: bigint | symbol }>(), + ); + }); + it("object with `Function | symbol`", () => { + const result = makeReadonly(objectWithFunctionOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly functionOrSymbol: (() => void) | symbol }>(), + ); + }); + it("object with `number | bigint | symbol`", () => { + const result = makeReadonly(objectWithNumberOrBigintOrSymbol); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly numberOrBigintOrSymbol: number | bigint | symbol }>(), + ); + }); + + it("object with function with properties", () => { + const result = makeReadonly(objectWithFunctionWithProperties); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly function: (() => number) & { + property: number; + }; + }>(), + ); + }); + it("object with object and function", () => { + const result = makeReadonly(objectWithObjectAndFunction); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly object: (() => number) & { + property: number; + }; + }>(), + ); + }); + + it("object with number key", () => { + const result = makeReadonly(objectWithNumberKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly 3: string }>()); + }); + it("object with symbol key", () => { + const result = makeReadonly(objectWithSymbolKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly [x: symbol]: string }>()); + }); + it("object with unique symbol key", () => { + const result = makeReadonly(objectWithUniqueSymbolKey); + assertIdenticalTypes(result, createInstanceOf<{ readonly [uniqueSymbol]: string }>()); + }); + + it("object with array of `number`s", () => { + const result = makeReadonly(objectWithArrayOfNumbers); + assertIdenticalTypes(result, createInstanceOf<{ readonly arrayOfNumbers: number[] }>()); + }); + it("object with array of `number | undefined`", () => { + const result = makeReadonly(objectWithArrayOfNumbersOrUndefined); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfNumbersOrUndefined: (number | undefined)[]; + }>(), + ); + }); + it("object with array of `bigint`s", () => { + const result = makeReadonly(objectWithArrayOfBigints); + assertIdenticalTypes(result, createInstanceOf<{ readonly arrayOfBigints: bigint[] }>()); + }); + it("object with array of `symbol`s", () => { + const result = makeReadonly(objectWithArrayOfSymbols); + assertIdenticalTypes(result, createInstanceOf<{ readonly arrayOfSymbols: symbol[] }>()); + }); + it("object with array of `unknown`", () => { + const result = makeReadonly(objectWithArrayOfUnknown); + assertIdenticalTypes(result, createInstanceOf<{ readonly arrayOfUnknown: unknown[] }>()); + }); + it("object with array of functions", () => { + const result = makeReadonly(objectWithArrayOfFunctions); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly arrayOfFunctions: (typeof aFunction)[] }>(), + ); + }); + it("object with array of functions with properties", () => { + const result = makeReadonly(objectWithArrayOfFunctionsWithProperties); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfFunctionsWithProperties: ((() => number) & { + property: number; + })[]; + }>(), + ); + }); + it("object with array of objects and functions", () => { + const result = makeReadonly(objectWithArrayOfObjectAndFunctions); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfObjectAndFunctions: ((() => number) & { + property: number; + })[]; + }>(), + ); + }); + it("object with array of `bigint`s or objects", () => { + const result = makeReadonly(objectWithArrayOfBigintOrObjects); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfBigintOrObjects: ( + | bigint + | { + property: string; + } + )[]; + }>(), + ); + }); + it("object with array of `symbol`s or objects", () => { + const result = makeReadonly(objectWithArrayOfSymbolOrObjects); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly arrayOfSymbolOrObjects: ( + | symbol + | { + property: string; + } + )[]; + }>(), + ); + }); + it("object with readonly array of `number`", () => { + const result = makeReadonly(objectWithReadonlyArrayOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly readonlyArrayOfNumbers: readonly number[] }>(), + ); + }); + + it("`string` indexed record of `number`s", () => { + const result = makeReadonly(stringRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly [x: string]: number; + }>(), + ); + }); + it("`string` indexed record of `unknown`s", () => { + const result = makeReadonly(stringRecordOfUnknown); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly [x: string]: unknown; + }>(), + ); + }); + it("`string`|`number` indexed record of `string`s", () => { + const result = makeReadonly(stringOrNumberRecordOfStrings); + assertIdenticalTypes( + result, + createInstanceOf>>(), + ); + }); + it("`string`|`number` indexed record of objects", () => { + const result = makeReadonly(stringOrNumberRecordOfObjects); + assertIdenticalTypes( + result, + createInstanceOf>>(), + ); + }); + it("`string` indexed record of `number`|`string`s with known properties", () => { + const result = makeReadonly(stringRecordOfNumbersOrStringsWithKnownProperties); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`string` indexed record of `unknown` and optional known properties", () => { + const result = makeReadonly(stringRecordOfUnknownWithOptionalKnownProperties); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`string`|`number` indexed record of `strings` with known `number` property (unassignable)", () => { + const result = makeReadonly(stringOrNumberRecordOfStringWithKnownNumber); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`Partial<>` `string` indexed record of `numbers`", () => { + // Warning: as of TypeScript 5.8.2, a Partial<> of an indexed type + // gains `| undefined` even under exactOptionalPropertyTypes=true. + // Preferred result is that there is no change applying Partial<>. + // In either case, this test can hold since there isn't a downside + // to allowing `undefined` in the result if that is how type is + // given since indexed properties are always inherently optional. + const result = makeReadonly(partialStringRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("`Partial<>` `string` indexed record of `numbers`", () => { + const result = makeReadonly(partialStringRecordOfUnknown); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("templated record of `numbers`", () => { + const result = makeReadonly(templatedRecordOfNumbers); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("templated record of `numbers`", () => { + const result = makeReadonly(mixedRecordOfUnknown); + assertIdenticalTypes(result, createInstanceOf>()); + }); + + it("object with recursion and `symbol`", () => { + const result = makeReadonly(objectWithSymbolOrRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + + it("object with recursion and handle", () => { + const result = makeReadonly(objectWithFluidHandleOrRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + + it("object with function object with recursion", () => { + const objectWithSelfRecursiveFunctionWithProperties = { + outerFnOjb: selfRecursiveFunctionWithProperties, + }; + const result = makeReadonly(objectWithSelfRecursiveFunctionWithProperties); + const expected = { + outerFnOjb: selfRecursiveFunctionWithProperties, + } as const; + assertIdenticalTypes(result, expected); + }); + it("object with object and function with recursion", () => { + const objectWithSelfRecursiveObjectAndFunction = { + outerFnOjb: selfRecursiveObjectAndFunction, + }; + const result = makeReadonly(objectWithSelfRecursiveObjectAndFunction); + const expected = { + outerFnOjb: selfRecursiveObjectAndFunction, + } as const; + assertIdenticalTypes(result, expected); + }); + + it("object with possible type recursion through union", () => { + const result = makeReadonly(objectWithPossibleRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("object with optional type recursion", () => { + const result = makeReadonly(objectWithOptionalRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("object with deep type recursion", () => { + const result = makeReadonly(objectWithEmbeddedRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("object with alternating type recursion", () => { + const result = makeReadonly(objectWithAlternatingRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + + it("object with inherited recursion and extended with mutable properties", () => { + const result = makeReadonly({ + outer: objectInheritingOptionalRecursionAndWithNestedSymbol, + }); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly outer: typeof objectInheritingOptionalRecursionAndWithNestedSymbol; + }>(), + ); + }); + + it("`string` indexed record of recursion or `number`", () => { + const result = makeReadonly(stringRecordWithRecursionOrNumber); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + + it("simple json (`JsonTypeWith`)", () => { + const result = makeReadonly(simpleJson); + assertIdenticalTypes(result, createInstanceOf>()); + }); + + it("simple non-null object json (`NonNullJsonObjectWith`)", () => { + const result = makeReadonly(jsonObject); + assertIdenticalTypes(result, createInstanceOf>()); + }); + + it("non-const enum", () => { + // Note: typescript doesn't do a great job checking that a generated type satisfies an enum + // type. The numeric indices are not checked. So far, most robust inspection is manual + // after any change. + const resultNumericRead = makeReadonly(NumericEnum); + assertIdenticalTypes(resultNumericRead, NumericEnum); + const resultStringRead = makeReadonly(StringEnum); + assertIdenticalTypes(resultStringRead, StringEnum); + const resultComputedRead = makeReadonly(ComputedEnum); + assertIdenticalTypes(resultComputedRead, ComputedEnum); + }); + + it("object with matched getter and setter", () => { + const result = makeReadonly(objectWithMatchedGetterAndSetterProperty); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + }); + + it("object with matched getter and setter implemented via value", () => { + const result = makeReadonly(objectWithMatchedGetterAndSetterPropertyViaValue); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + }); + it("object with mismatched getter and setter implemented via value", () => { + const result = makeReadonly(objectWithMismatchedGetterAndSetterPropertyViaValue); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + // @ts-expect-error Cannot assign to 'property' because it is a read-only property + result.property = -1; + }); + it("object with mismatched getter and setter", () => { + const result = makeReadonly(objectWithMismatchedGetterAndSetterProperty); + assertIdenticalTypes( + result, + createInstanceOf<{ + get property(): number; + }>(), + ); + + assert.throws(() => { + // @ts-expect-error Cannot assign to 'property' because it is a read-only property + result.property = -1; + }, new Error( + "ClassImplementsObjectWithMismatchedGetterAndSetterProperty writing 'property' as -1", + )); + }); + + describe("class instance", () => { + it("with public data", () => { + const result = makeReadonly(classInstanceWithPublicData); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("with public method", () => { + const result = makeReadonly(classInstanceWithPublicMethod); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + result satisfies typeof classInstanceWithPublicMethod; + }); + it("with public data and is function", () => { + const result = makeReadonly(classInstanceWithPublicDataAndIsFunction); + assertIdenticalTypes( + result, + createInstanceOf<(() => 26) & { readonly public: string }>(), + ); + }); + }); + + it("object with optional property (remains optional)", () => { + const result = makeReadonly(objectWithOptionalNumberNotPresent); + assertIdenticalTypes(result, createInstanceOf<{ readonly optNumber?: number }>()); + }); + }); + + describe("function & object intersections result in immutable object portion", () => { + it("function with properties", () => { + const result = makeReadonly(functionWithProperties); + assertIdenticalTypes( + result, + createInstanceOf< + (() => number) & { + readonly property: number; + } + >(), + ); + }); + it("object and function", () => { + const result = makeReadonly(objectAndFunction); + assertIdenticalTypes( + result, + createInstanceOf< + { + readonly property: number; + } & (() => number) + >(), + ); + }); + it("function with class instance with private data", () => { + const result = makeReadonly(functionObjectWithPrivateData); + assertIdenticalTypes( + result, + createInstanceOf< + (() => 23) & { + readonly public: string; + } + >(), + ); + }); + it("function with class instance with public data", () => { + const result = makeReadonly(functionObjectWithPublicData); + assertIdenticalTypes( + result, + createInstanceOf< + (() => 24) & { + readonly public: string; + } + >(), + ); + }); + it("function object with recursion", () => { + const result = makeReadonly(selfRecursiveFunctionWithProperties); + assertIdenticalTypes( + result, + createInstanceOf<(() => number) & Readonly>(), + ); + }); + it("object and function with recursion", () => { + const result = makeReadonly(selfRecursiveObjectAndFunction); + assertIdenticalTypes( + result, + createInstanceOf<(() => number) & Readonly>(), + ); + }); + }); + + describe("read-only objects are preserved", () => { + it("object with `readonly`", () => { + const result = makeReadonly(objectWithReadonly); + assertIdenticalTypes(result, objectWithReadonly); + }); + + it("object with getter implemented via value", () => { + const result = makeReadonly(objectWithGetterViaValue); + assertIdenticalTypes(result, objectWithGetterViaValue); + }); + it("object with `readonly` implemented via getter", () => { + const result = makeReadonly(objectWithReadonlyViaGetter); + assertIdenticalTypes(result, objectWithReadonlyViaGetter); + }); + + it("object with getter", () => { + const result = makeReadonly(objectWithGetter); + assertIdenticalTypes(result, objectWithGetter); + + assert.throws(() => { + // @ts-expect-error Cannot assign to 'getter' because it is a read-only property. + objectWithGetter.getter = -1; + }, new TypeError( + "Cannot set property getter of # which has only a getter", + )); + assert.throws(() => { + // @ts-expect-error Cannot assign to 'getter' because it is a read-only property. + result.getter = -1; + }, new TypeError( + "Cannot set property getter of # which has only a getter", + )); + }); + + it("simple read-only json (`ReadonlyJsonTypeWith`)", () => { + const result = makeReadonly(simpleImmutableJson); + assertIdenticalTypes(result, simpleImmutableJson); + }); + + it("simple read-only non-null object json (`NonNullJsonObjectWith`)", () => { + const result = makeReadonly(immutableJsonObject); + assertIdenticalTypes(result, immutableJsonObject); + }); + }); + + describe("built-in or common class instances", () => { + it("ErasedType is preserved", () => { + const result = makeReadonly(erasedType); + assertIdenticalTypes(result, erasedType); + }); + + describe("`IFluidHandle` becomes `Readonly>`", () => { + it("`IFluidHandle`", () => { + const result = makeReadonly(fluidHandleToNumber); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("`IFluidHandle<{...}>` generic remains intact when disabled", () => { + const result = makeReadonlyNoGenericsDeepening(fluidHandleToRecord); + assertIdenticalTypes(result, createInstanceOf>()); + }); + it("`IFluidHandle<{...}>` generic becomes shallowly immutable by default", () => { + const result = makeReadonly(fluidHandleToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + Readonly< + IFluidHandle<{ + readonly [p: string]: { x: number; y: number }; + }> + > + >(), + ); + }); + it("object with `IFluidHandle`", () => { + const result = makeReadonly(objectWithFluidHandle); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly handle: IFluidHandle; + }>(), + ); + }); + it("object with `IFluidHandle` or recursion", () => { + const result = makeReadonly(objectWithFluidHandleOrRecursion); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("read-only object with `FluidHandle` or recursion", () => { + const result = makeReadonly(readonlyObjectWithFluidHandleOrRecursion); + assertIdenticalTypes(result, readonlyObjectWithFluidHandleOrRecursion); + }); + }); + + describe("Branded primitive is preserved", () => { + it("`number & BrandedType`", () => { + const result = makeReadonly(brandedNumber); + assertIdenticalTypes(result, brandedNumber); + }); + it("`string & BrandedType`", () => { + const result = makeReadonly(brandedString); + assertIdenticalTypes(result, brandedString); + }); + it("object with `number & BrandedType`", () => { + const result = makeReadonly(objectWithBrandedNumber); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + it("object with `string & BrandedType`", () => { + const result = makeReadonly(objectWithBrandedString); + assertIdenticalTypes( + result, + createInstanceOf>(), + ); + }); + }); + + describe("that are mutable become immutable version", () => { + describe("Map becomes ReadonlyMap", () => { + it("Map", () => { + const result = makeReadonly(mapOfStringsToNumbers); + assertIdenticalTypes(result, readonlyMapOfStringsToNumbers); + + mapOfStringsToNumbers satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof mapOfStringsToNumbers; + }); + it("Map generics become shallowly immutable when enabled", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics(mapOfPointToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlyMap< + Readonly, + { + readonly [p: string]: Point; + } + > + >(), + ); + + mapOfPointToRecord satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof mapOfPointToRecord; + }); + it("Map generics are intact by default", () => { + const result = makeReadonly(mapOfPointToRecord); + assertIdenticalTypes(result, readonlyMapOfPointToRecord); + }); + }); + describe("Set becomes ReadonlySet", () => { + it("Set", () => { + const result = makeReadonly(setOfNumbers); + assertIdenticalTypes(result, readonlySetOfNumbers); + + setOfNumbers satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof setOfNumbers; + }); + it("Set generics become shallowly immutable when enabled", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics(setOfRecords); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlySet<{ + readonly [p: string]: Point; + }> + >(), + ); + + setOfRecords satisfies typeof result; + // @ts-expect-error methods are missing, but required + result satisfies typeof setOfRecords; + }); + it("Set generics are intact by default", () => { + const result = makeReadonly(setOfRecords); + assertIdenticalTypes(result, readonlySetOfRecords); + }); + }); + }); + describe("that are immutable make generics shallowly immutable when enabled", () => { + it("ReadonlyMap", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics( + readonlyMapOfStringsToNumbers, + ); + assertIdenticalTypes(result, readonlyMapOfStringsToNumbers); + result satisfies typeof readonlyMapOfStringsToNumbers; + }); + it("ReadonlyMap", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics(readonlyMapOfPointToRecord); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlyMap< + Readonly, + { + readonly [p: string]: Point; + } + > + >(), + ); + result satisfies typeof readonlyMapOfPointToRecord; + }); + it("ReadonlySet", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics(readonlySetOfNumbers); + assertIdenticalTypes(result, readonlySetOfNumbers); + result satisfies typeof readonlySetOfNumbers; + }); + it("ReadonlySet", () => { + const result = makeReadonlyDeepeningAllSupportedGenerics(readonlySetOfRecords); + assertIdenticalTypes( + result, + createInstanceOf< + ReadonlySet<{ + readonly [p: string]: Point; + }> + >(), + ); + result satisfies typeof readonlySetOfRecords; + }); + }); + describe("that are immutable keep generics intact by default", () => { + it("ReadonlyMap", () => { + const result = makeReadonly(readonlyMapOfPointToRecord); + assertIdenticalTypes(result, readonlyMapOfPointToRecord); + result satisfies typeof readonlyMapOfPointToRecord; + }); + it("ReadonlySet", () => { + const result = makeReadonly(readonlySetOfRecords); + assertIdenticalTypes(result, readonlySetOfRecords); + result satisfies typeof readonlySetOfRecords; + }); + }); + }); + + describe("partially supported object types are modified", () => { + describe("class instance non-public properties are removed", () => { + it("with private method", () => { + const result = makeReadonly(classInstanceWithPrivateMethod); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error getSecret is missing, but required + result satisfies typeof classInstanceWithPrivateMethod; + // @ts-expect-error getSecret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateMethod); + }); + it("with private getter", () => { + const result = makeReadonly(classInstanceWithPrivateGetter); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateGetter; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateGetter); + }); + it("with private setter", () => { + const result = makeReadonly(classInstanceWithPrivateSetter); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateSetter; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateSetter); + }); + it("with private data", () => { + const result = makeReadonly(classInstanceWithPrivateData); + assertIdenticalTypes( + result, + createInstanceOf<{ + readonly public: string; + }>(), + ); + // @ts-expect-error secret is missing, but required + result satisfies typeof classInstanceWithPrivateData; + // @ts-expect-error secret is missing, but required + assertIdenticalTypes(result, classInstanceWithPrivateData); + }); + it("with private data and is function", () => { + const result = makeReadonly(classInstanceWithPrivateDataAndIsFunction); + assertIdenticalTypes( + result, + createInstanceOf<{ readonly public: string } & (() => 25)>(), + ); + }); + }); + + // A class branded object cannot be detected without knowing the branding type. + // And will class privates removed just the original type is made immutable. + describe("branded non-primitive types lose branding", () => { + // class branding with `object` is just the class branding and produces + // an empty object, {}, which happens to be a special any object type. + it("`object & BrandedType`", () => { + const result = makeReadonly(brandedObject); + assertIdenticalTypes(result, {}); + }); + it("`object & BrandedType`", () => { + const result = makeReadonly(brandedObjectWithString); + assertIdenticalTypes(result, createInstanceOf<{ readonly string: string }>()); + }); + }); + }); + + describe("types that cannot be made immutable are unchanged", () => { + it("`any`", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const any: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result = makeReadonly(any); + assertIdenticalTypes(result, any); + }); + it("`unknown`", () => { + const result = makeReadonly(unknownValueOfSimpleRecord); + assertIdenticalTypes(result, unknownValueOfSimpleRecord); + }); + it("`object` (plain object)", () => { + const result = makeReadonly(object); + assertIdenticalTypes(result, object); + }); + it("`null`", () => { + /* eslint-disable unicorn/no-null */ + const result = makeReadonly(null); + assertIdenticalTypes(result, null); + /* eslint-enable unicorn/no-null */ + }); + it("`void`", () => { + const result = makeReadonly(voidValue); + assertIdenticalTypes(result, voidValue); + }); + it("`never`", () => { + const result = makeReadonly(never); + assertIdenticalTypes(result, never); + }); + }); + + describe("unsupported types", () => { + // These cases are demonstrating defects within the current implementation. + // They show "allowed" incorrect use and the unexpected results. + describe("known defect expectations", () => { + it("object with setter becomes read-only property", () => { + const result = makeReadonly(objectWithSetter); + // @ts-expect-error `setter` is no longer mutable + assertIdenticalTypes(result, objectWithSetter); + assertIdenticalTypes(result, createInstanceOf<{ readonly setter: string }>()); + + // Read from setter only produces `undefined` but is typed as `string`. + const originalSetterValue = objectWithSetter.setter; + assert.equal(originalSetterValue, undefined); + // Read from modified type is the same (as it is the same object). + const resultSetterValue = result.setter; + assert.equal(resultSetterValue, undefined); + + assert.throws(() => { + objectWithSetter.setter = "test string 1"; + }, new Error("ClassImplementsObjectWithSetter writing 'setter' as test string 1")); + assert.throws(() => { + // @ts-expect-error Cannot assign to 'setter' because it is a read-only property. + result.setter = "test string 2"; + }, new Error("ClassImplementsObjectWithSetter writing 'setter' as test string 2")); + }); + }); + + it("unique symbol becomes `symbol`", () => { + const result = makeReadonly(uniqueSymbol); + // @ts-expect-error `uniqueSymbol` is no longer a specific (unique) symbol + assertIdenticalTypes(result, uniqueSymbol); + // mysteriously becomes `symbol` (can't be preserved) + assertIdenticalTypes(result, symbol); + }); + }); +}); diff --git a/packages/common/core-interfaces/src/test/testValues.ts b/packages/common/core-interfaces/src/test/testValues.ts index 4f213cfc6190..9252b11ff954 100644 --- a/packages/common/core-interfaces/src/test/testValues.ts +++ b/packages/common/core-interfaces/src/test/testValues.ts @@ -5,16 +5,23 @@ import { assertIdenticalTypes } from "./testUtils.js"; -import type { IFluidHandle, IFluidHandleErased } from "@fluidframework/core-interfaces"; +import type { + ErasedType, + IFluidHandle, + IFluidHandleErased, +} from "@fluidframework/core-interfaces"; import { fluidHandleSymbol } from "@fluidframework/core-interfaces"; +import type { ReadonlyNonNullJsonObjectWith } from "@fluidframework/core-interfaces/internal"; import type { JsonTypeWith, InternalUtilityTypes, + ReadonlyJsonTypeWith, NonNullJsonObjectWith, } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; /* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ export const boolean: boolean = true as boolean; // Use `as` to avoid type conversion to `true` export const number: number = 0; @@ -27,7 +34,7 @@ export const aFunction = (): any => {}; export const unknownValueOfSimpleRecord = { key: "value" } as unknown; export const unknownValueWithBigint = { bigint: 1n } as unknown; export const voidValue = null as unknown as void; -const never = null as never; +export const never = null as never; export const stringOrSymbol = Symbol("objectSymbol") as string | symbol; export const bigintOrString = "not bigint" as string | bigint; @@ -134,7 +141,7 @@ export const objectWithArrayOfFunctions = { arrayOfFunctions }; export const objectWithArrayOfFunctionsWithProperties = { arrayOfFunctionsWithProperties }; export const objectWithArrayOfObjectAndFunctions = { arrayOfObjectAndFunctions }; export const objectWithArrayOfBigintOrObjects = { arrayOfBigintOrObjects }; -export const objectWithArrayOfSymbolsOrObjects = { arrayOfSymbolOrObjects }; +export const objectWithArrayOfSymbolOrObjects = { arrayOfSymbolOrObjects }; export const objectWithReadonlyArrayOfNumbers = { readonlyArrayOfNumbers }; export const objectWithUnknown = { unknown: "value" as unknown }; @@ -264,6 +271,10 @@ export const stringRecordOfNumbers: Record = { key: 0 }; export const stringRecordOfUndefined: Record = { key: undefined }; export const stringRecordOfUnknown: Record = { key: 0 }; export const stringOrNumberRecordOfStrings: Record = { 5: "value" }; +export const stringOrNumberRecordOfObjects: Record = { + 8: { string: "string value" }, + knownNumber: { string: "4" }, +}; // Ideally TypeScript would not allow this assignment. Index signatures are // inherently optional and modification via `Partial` should not modify the // type (particularly under exactOptionalPropertyTypes=true). @@ -297,7 +308,6 @@ export const mixedRecordOfUnknown: Record< }; // Must use `type` over `interface` to enable intersection with `Record<>`. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions type KnownStringAndNumber = { knownString: string; knownNumber: number }; export const stringRecordOfNumbersOrStringsWithKnownProperties: InternalUtilityTypes.FlattenIntersection< Record & KnownStringAndNumber @@ -338,8 +348,6 @@ export const stringOrNumberRecordOfUndefinedWithKnownNumber = { // #region Recursive types -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - type ObjectWithPossibleRecursion = { [x: string]: ObjectWithPossibleRecursion | string; }; @@ -352,9 +360,18 @@ export type ObjectWithOptionalRecursion = { export const objectWithOptionalRecursion: ObjectWithOptionalRecursion = { recursive: {}, }; +export type ReadonlyObjectWithOptionalRecursion = { + readonly recursive?: ReadonlyObjectWithOptionalRecursion; +}; +export const readonlyObjectWithOptionalRecursion: ReadonlyObjectWithOptionalRecursion = + objectWithOptionalRecursion; + export const objectWithEmbeddedRecursion = { outer: objectWithOptionalRecursion, }; +export const readonlyObjectWithEmbeddedRecursion = { + outer: readonlyObjectWithOptionalRecursion, +} as const; export const objectWithSelfReference: ObjectWithOptionalRecursion = {}; objectWithSelfReference.recursive = objectWithSelfReference; @@ -373,6 +390,14 @@ export const objectWithAlternatingRecursion: ObjectWithAlternatingRecursionA = { }, }, }; +type ReadonlyObjectWithAlternatingRecursionA = { + readonly recurseA: ReadonlyObjectWithAlternatingRecursionB | number; +}; +type ReadonlyObjectWithAlternatingRecursionB = { + readonly recurseB: ReadonlyObjectWithAlternatingRecursionA | "stop"; +}; +export const readonlyObjectWithAlternatingRecursion: ReadonlyObjectWithAlternatingRecursionA = + objectWithAlternatingRecursion; export type ObjectWithSymbolOrRecursion = { recurse: ObjectWithSymbolOrRecursion | symbol; @@ -380,6 +405,11 @@ export type ObjectWithSymbolOrRecursion = { export const objectWithSymbolOrRecursion: ObjectWithSymbolOrRecursion = { recurse: { recurse: Symbol("stop") }, }; +type ReadonlyObjectWithSymbolOrRecursion = { + readonly recurse: ReadonlyObjectWithSymbolOrRecursion | symbol; +}; +export const readonlyObjectWithSymbolOrRecursion: ReadonlyObjectWithSymbolOrRecursion = + objectWithSymbolOrRecursion; type ObjectWithFluidHandleOrRecursion = { recurseToHandle: ObjectWithFluidHandleOrRecursion | IFluidHandle; @@ -387,6 +417,13 @@ type ObjectWithFluidHandleOrRecursion = { export const objectWithFluidHandleOrRecursion: ObjectWithFluidHandleOrRecursion = { recurseToHandle: { recurseToHandle: "fake-handle" as unknown as IFluidHandle }, }; +type ReadonlyObjectWithFluidHandleOrRecursion = { + readonly recurseToHandle: + | ReadonlyObjectWithFluidHandleOrRecursion + | Readonly>; +}; +export const readonlyObjectWithFluidHandleOrRecursion: ReadonlyObjectWithFluidHandleOrRecursion = + objectWithFluidHandleOrRecursion; export const objectWithUnknownAdjacentToOptionalRecursion = { unknown: unknownValueOfSimpleRecord, @@ -414,6 +451,18 @@ type ObjectWithOptionalUnknownInOptionalRecursion = { export const objectWithOptionalUnknownInOptionalRecursion: ObjectWithOptionalUnknownInOptionalRecursion = objectWithUnknownInOptionalRecursion; +type StringRecordWithRecursionOrNumber = { + [x: string]: StringRecordWithRecursionOrNumber | number; +}; +export const stringRecordWithRecursionOrNumber: StringRecordWithRecursionOrNumber = { + outer: { inner: 5 }, +}; +type ReadonlyStringRecordWithRecursionOrNumber = { + readonly [x: string]: ReadonlyStringRecordWithRecursionOrNumber | number; +}; +export const readonlyStringRecordWithRecursionOrNumber: ReadonlyStringRecordWithRecursionOrNumber = + stringRecordWithRecursionOrNumber; + export type SelfRecursiveFunctionWithProperties = (() => number) & { recurse?: SelfRecursiveFunctionWithProperties; }; @@ -430,7 +479,16 @@ export const selfRecursiveObjectAndFunction: SelfRecursiveObjectAndFunction = // assignment of one to the other. assertIdenticalTypes(selfRecursiveObjectAndFunction, selfRecursiveFunctionWithProperties); -/* eslint-enable @typescript-eslint/consistent-type-definitions */ +export type ReadonlySelfRecursiveFunctionWithProperties = (() => number) & { + readonly recurse?: ReadonlySelfRecursiveFunctionWithProperties; +}; +export const readonlySelfRecursiveFunctionWithProperties: ReadonlySelfRecursiveFunctionWithProperties = + selfRecursiveFunctionWithProperties; +export type ReadonlySelfRecursiveObjectAndFunction = { + readonly recurse?: ReadonlySelfRecursiveObjectAndFunction; +} & (() => number); +export const readonlySelfRecursiveObjectAndFunction: ReadonlySelfRecursiveObjectAndFunction = + selfRecursiveObjectAndFunction; interface ObjectInheritingOptionalRecursionAndWithNestedSymbol extends ObjectWithOptionalRecursion { @@ -453,8 +511,10 @@ export const objectInheritingOptionalRecursionAndWithNestedSymbol: ObjectInherit }; export const simpleJson: JsonTypeWith = { a: [{ b: { b2: 8 }, c: true }] }; +export const simpleImmutableJson: ReadonlyJsonTypeWith = simpleJson; export const jsonObject: NonNullJsonObjectWith = [simpleJson]; +export const immutableJsonObject: ReadonlyNonNullJsonObjectWith = jsonObject; // #endregion @@ -535,13 +595,23 @@ export const classInstanceWithPublicDataAndIsFunction = Object.assign( () => 26, ); -// #region Common Class types +// #region Built-in Class types + +export type Point = { x: number; y: number }; +export type StringRecordOfPoints = { + [p: string]: Point; +}; export const mapOfStringsToNumbers = new Map(); export const readonlyMapOfStringsToNumbers: ReadonlyMap = mapOfStringsToNumbers; +export const mapOfPointToRecord = new Map(); +export const readonlyMapOfPointToRecord: ReadonlyMap = + mapOfPointToRecord; export const setOfNumbers = new Set(); export const readonlySetOfNumbers: ReadonlySet = setOfNumbers; +export const setOfRecords = new Set(); +export const readonlySetOfRecords: ReadonlySet = setOfRecords; // #endregion @@ -569,19 +639,32 @@ export const objectWithBrandedString = { brandedString }; // #region Fluid types -export const fluidHandleToNumber: IFluidHandle = { - isAttached: false, - async get(): Promise { - throw new Error("Function not implemented."); - }, - [fluidHandleSymbol]: undefined as unknown as IFluidHandleErased, -}; +function makeFauxFluidHandle(): IFluidHandle { + return { + isAttached: false, + async get(): Promise { + throw new Error("Function not implemented."); + }, + [fluidHandleSymbol]: undefined as unknown as IFluidHandleErased, + }; +} + +export const fluidHandleToNumber = makeFauxFluidHandle(); +export const fluidHandleToRecord = makeFauxFluidHandle<{ + [p: string]: { x: number; y: number }; +}>(); export const objectWithFluidHandle = { handle: fluidHandleToNumber, }; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface TestErasedType extends ErasedType {} + +export const erasedType: TestErasedType = 0 as unknown as TestErasedType; + // #endregion +/* eslint-enable @typescript-eslint/consistent-type-definitions */ /* eslint-enable unicorn/no-null */ /* eslint-enable jsdoc/require-jsdoc */