diff --git a/types/seamless-immutable/index.d.ts b/types/seamless-immutable/index.d.ts index 31c6eea8be..853b8e2113 100644 --- a/types/seamless-immutable/index.d.ts +++ b/types/seamless-immutable/index.d.ts @@ -32,8 +32,8 @@ declare namespace SeamlessImmutable { prototype?: any; } - interface AsMutableOptions { - deep: boolean; + interface AsMutableOptions { + deep: TDeep; } interface ImmutableObjectMixin { @@ -63,7 +63,9 @@ declare namespace SeamlessImmutable { getIn(propertyPath: string[]): Immutable; getIn(propertyPath: string[], defaultValue: TValue): Immutable; - asMutable(opts?: AsMutableOptions): T; + asMutable(opts?: AsMutableOptions): { [K in keyof T]: Immutable }; + asMutable(opts: AsMutableOptions): T; + asMutable(opts: AsMutableOptions): T | { [K in keyof T]: Immutable }; merge(part: DeepPartial, config?: MergeConfig): Immutable; @@ -91,29 +93,38 @@ declare namespace SeamlessImmutable { type ImmutableObject = ImmutableObjectMixin & { readonly [P in keyof T]: Immutable }; /** An ImmutableArray provides read-only access to the array elements, and provides functions (such as `map()`) that return immutable data structures. */ - type ImmutableArray = Readonly & ImmutableArray.Additions & ImmutableArray.Overrides>; + type ImmutableArray = Readonly> & ImmutableArray.Additions & ImmutableArray.Overrides & ImmutableArray.ReadOnlyIndexer; namespace ImmutableArray { /** New methods added by seamless-immutable. */ interface Additions { - asMutable(opts?: AsMutableOptions): T[]; + asMutable(opts?: AsMutableOptions): Array>; + asMutable(opts: AsMutableOptions): T[]; + asMutable(opts: AsMutableOptions): T[] | Array>; + asObject(toKeyValue: (item: T) => [K, U[K]]): Immutable; flatMap(mapFunction: (item: T) => TTarget): Immutable; } /** Custom implementation of the array functions, which return Immutable. */ interface Overrides { - map(mapFuction: (item: T) => TTarget): Immutable; - filter(filterFunction: (item: T) => boolean): Immutable; + forEach(callbackfn: (value: Immutable, index: number, array: Immutable) => void, thisArg?: any): void; + map(mapFuction: (item: Immutable) => TTarget): Immutable; + filter(filterFunction: (item: Immutable) => boolean): Immutable; slice(start?: number, end?: number): Immutable; - concat(...arr: Array>): Immutable; - reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): Immutable; - reduce(callbackfn: (previousValue: TTarget, currentValue: T, currentIndex: number, array: T[]) => TTarget, initialValue?: TTarget): Immutable; - reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): Immutable; - reduceRight(callbackfn: (previousValue: TTarget, currentValue: T, currentIndex: number, array: T[]) => TTarget, initialValue?: TTarget): Immutable; + concat(...arr: Array|Array>|Immutable>): Immutable; + reduce(callbackfn: (previousValue: Immutable, currentValue: Immutable, currentIndex: number, array: Immutable) => T): Immutable; + reduce(callbackfn: (previousValue: TTarget, currentValue: Immutable, currentIndex: number, array: Immutable) => TTarget, initialValue?: TTarget): Immutable; + reduceRight(callbackfn: (previousValue: Immutable, currentValue: Immutable, currentIndex: number, array: Immutable) => T): Immutable; + reduceRight(callbackfn: (previousValue: TTarget, currentValue: Immutable, currentIndex: number, array: Immutable) => TTarget, initialValue?: TTarget): Immutable; + } + + /** Merging this into Overrides breaks stuff, so this is split out */ + interface ReadOnlyIndexer { + readonly [key: number]: Immutable; } /** These methods are banned by seamless-immutable. */ - type MutatingArrayMethods = Extract; + type MutatingArrayMethods = Extract; /** NOTE: These methods mutate data, but seamless-immutable does not ban them. We will ban them in our type definitions. */ type AdditionalMutatingArrayMethods = Extract; @@ -160,7 +171,7 @@ declare namespace SeamlessImmutable { function from(obj: T, options?: Options): Immutable; - function isImmutable(target: any): boolean; + function isImmutable(target: T | Immutable): target is Immutable; function ImmutableError(message: string): Error; function replace(obj: Immutable, valueObj: S, options?: ReplaceConfig): Immutable; diff --git a/types/seamless-immutable/seamless-immutable-tests.ts b/types/seamless-immutable/seamless-immutable-tests.ts index 773a7b9320..e2982b9e08 100644 --- a/types/seamless-immutable/seamless-immutable-tests.ts +++ b/types/seamless-immutable/seamless-immutable-tests.ts @@ -15,6 +15,13 @@ interface ExtendedUser extends User { address: Address; } +// A result type for asMutable({ deep: false } | undefined) +interface NonDeepMutableExtendedUser { + firstName: string; + lastName: string; + address: Immutable.Immutable
; +} + // // Constructors // --------------------------------------------------------------- @@ -89,6 +96,27 @@ interface ExtendedUser extends User { { const array: Immutable.Immutable = Immutable.from([ { firstName: 'Angry', lastName: 'Monkey' } ]); + const immutableElement: Immutable.Immutable = array[0]; + // $ExpectError + immutableElement.firstName = 'Krusty'; + + const asMutableDefault: Array> = array.asMutable(); + // Can't mutate beyond 1 level deep (in this case only the array itself is mutable, not the items) + // $ExpectError + asMutableDefault[0].firstName = 'Krusty'; + + const asMutableNotDeep: Array> = array.asMutable({ deep: false }); + // Can't mutate beyond 1 level deep (in this case only the array itself is mutable, not the items) + // $ExpectError + asMutableNotDeep[0].firstName = 'Krusty'; + + // Can mutate at any depth + const asMutableDeep: User[] = array.asMutable({ deep: true }); + asMutableDeep[0].firstName = 'Krusty'; + + const opaqueMutableParam = { deep: true }; + const asMutableOpaque: Array> = array.asMutable(opaqueMutableParam); + // keys. Call the mutable array's 'keys' to ensure compatability const mutableKeys = array.asMutable().keys(); const keys: typeof mutableKeys = array.keys(); @@ -119,7 +147,7 @@ interface ExtendedUser extends User { slice4.asMutable(); // concat. Call the mutable array's 'concat' with the same args to ensure compatability. Make sure the output array is immutable. - array.asMutable().concat({ firstName: 'Happy', lastName: 'Cat' }); + array.asMutable().concat(Immutable({ firstName: 'Happy', lastName: 'Cat' })); const concat: Immutable.Immutable = array .concat({ firstName: 'Happy', lastName: 'Cat' }, { firstName: 'Silly', lastName: 'Cat' }) .concat([{firstName: 'saucy', lastName: 'Cat'}], {firstName: 'Fussy', lastName: 'Cat'}) @@ -204,8 +232,25 @@ interface ExtendedUser extends User { const updatedUser12: Immutable.Immutable = immutableUserEx.setIn([ data.propertyId, 'line1' ], 'Small house'); // asMutable - const mutableUser21: User = immutableUser.asMutable(); - const mutableUser22: User = immutableUser.asMutable({ deep: true }); + // Can't mutate beyond 1 level deep + const mutableUser21 = immutableUserEx.asMutable(); + mutableUser21.firstName = 'Krusty'; + // $ExpectError + mutableUser21.address.line1 = 'Example'; + + // Can't mutate beyond 1 level deep + const mutableUser22 = immutableUserEx.asMutable({ deep: false }); + mutableUser22.firstName = 'Krusty'; + // $ExpectError + mutableUser22.address.line1 = 'Example'; + + // Can mutate at any depth + const mutableUser23: ExtendedUser = immutableUserEx.asMutable({ deep: true }); + mutableUser23.firstName = 'Krusty'; + mutableUser23.address.line1 = 'Example'; + + const opaqueMutableParam = { deep: true }; // treats as { deep: boolean } + const mutableUser24: ExtendedUser | NonDeepMutableExtendedUser = immutableUserEx.asMutable(opaqueMutableParam); // merge: merged part is strongly checked as a deeply partial object const mergedUser: Immutable.Immutable = immutableUserEx.merge({ address: { line1: 'Small house' }, firstName: 'Jack' });