From ba96b0832c7bafb5d3f1f1205ca66cfe53cd7c80 Mon Sep 17 00:00:00 2001 From: Paul Huynh Date: Mon, 14 Jan 2019 09:59:52 +1100 Subject: [PATCH] Improve seamless-immutable definitions - Prevent making already-immutable objects immutable again - Do not allow certain types (number, string, functions) from being made immutable - Introduce ImmutableDate - Add missing immutable-wrapper methods to ImmutableArray - Basic support for Promises --- types/seamless-immutable/index.d.ts | 77 +++++++++++-- .../seamless-immutable-tests.ts | 101 ++++++++++++++++++ 2 files changed, 170 insertions(+), 8 deletions(-) diff --git a/types/seamless-immutable/index.d.ts b/types/seamless-immutable/index.d.ts index b77e20d196..f5d3a2c3a3 100644 --- a/types/seamless-immutable/index.d.ts +++ b/types/seamless-immutable/index.d.ts @@ -4,12 +4,16 @@ // Stepan Burguchev // Geir Sagberg // Richard Honor +// Paul Huynh // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 export = SeamlessImmutable; declare namespace SeamlessImmutable { + /** From type T, take all properties except those specified by K. */ + type Omit = Pick>; + type DeepPartial = { [P in keyof T]?: DeepPartial; }; @@ -84,18 +88,75 @@ declare namespace SeamlessImmutable { replace(valueObj: S, options?: ReplaceConfig): Immutable; } + type ImmutableObject = ImmutableObjectMixin & { readonly [P in keyof T]: Immutable }; - interface ImmutableArrayMixin> { - asMutable(opts?: AsMutableOptions): T; - asObject(toKeyValue: (item: T[0]) => [string, any]): Immutable; - flatMap(mapFunction: (item: T[0]) => TTarget): 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>; + namespace ImmutableArray { + /** New methods added by seamless-immutable. */ + interface Additions { + asMutable(opts?: AsMutableOptions): T[]; + asObject(toKeyValue: (item: T) => [string, any]): 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; + slice(start?: number, end?: number): Immutable; + concat(...arr: T[]): 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; + } + + /** These methods are banned by seamless-immutable. */ + 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; + + /** The remaining properties on Array, after we remove the mutating functions and the wrapped non-mutating functions. */ + type Remaining = Omit>; } - type BaseImmutable = T extends any[] ? ImmutableArrayMixin : ImmutableObjectMixin; + /** An ImmutableDate disables the use of mutating functions like `setDate` and `setFullYear`. */ + type ImmutableDate = ImmutableDate.Remaining & ImmutableDate.Additions; + namespace ImmutableDate { + /** New functions added by seamless-immutable. */ + interface Additions { + asMutable(): Date; + } - type Immutable = { - readonly [P in keyof T]: T[P] extends object ? Immutable : T[P]; - } & BaseImmutable; + // These methods are banned by seamless-immutable + type MutatingDateMethods = Extract; + + /** Only allows Date methods, which are the getters. */ + type Remaining = Omit; + } + + type Immutable = + T extends Promise ? Promise> : + Immutable.MakeImmutable; + namespace Immutable { + type AnyFunction = (...args: any[]) => any; + + type AlreadyImmutable = ImmutableObject | ImmutableArray | ImmutableDate; + + type Primitive = boolean | number | string | symbol | AnyFunction | undefined | null; + + type CannotMakeImmutable = AlreadyImmutable | Primitive; + + type MakeImmutable = + T extends CannotMakeImmutable ? T : + T extends Array ? ImmutableArray : + T extends Date ? ImmutableDate : + ImmutableObject; + } function from(obj: T, options?: Options): Immutable; diff --git a/types/seamless-immutable/seamless-immutable-tests.ts b/types/seamless-immutable/seamless-immutable-tests.ts index 0f24523963..5b1a33d0bb 100644 --- a/types/seamless-immutable/seamless-immutable-tests.ts +++ b/types/seamless-immutable/seamless-immutable-tests.ts @@ -36,6 +36,37 @@ interface ExtendedUser extends User { lastName: 'Monkey' }); const error: Error = Immutable.ImmutableError('error'); + + const date: Immutable.ImmutableDate = Immutable(new Date()); + + // Constructing with a promise wraps the result value as an immutable + const promise = new Promise(resolve => resolve({ + firstName: 'Angry', + lastName: 'Monkey', + })); + const immutablePromise = Immutable(promise); + immutablePromise.then((user: Immutable.Immutable) => user.asMutable()); + + // Construction with Immutable() multiple times only creates an Immutable once + const user3: Immutable.Immutable = Immutable(Immutable(Immutable({ + firstName: 'Angry', + lastName: 'Monkey', + }))); + user3.asMutable(); + // Can't call asMutable() multiple times since there is only one level of immutability + // user3.asMutable().asMutable(); + + // Primitives are not made immutable + const str: string = Immutable("Hello World"); + const num: number = Immutable(123); + const bool: boolean = Immutable(true); + const sym: symbol = Immutable(Symbol("A symbol")); + const undef: undefined = Immutable(undefined); + const nul: null = Immutable(null); + const fun: () => User = Immutable((): User => ({ + firstName: 'Angry', + lastName: 'Monkey', + })); } // @@ -58,6 +89,58 @@ interface ExtendedUser extends User { { const array: Immutable.Immutable = Immutable.from([ { firstName: 'Angry', lastName: 'Monkey' } ]); + // keys. Call the mutable array's 'keys' to ensure compatability + const mutableKeys = array.asMutable().keys(); + const keys: typeof mutableKeys = array.keys(); + + // map. Call the mutable array's 'map' with the same function to ensure compatability. Make sure the output array is immutable. + interface FirstName { firstNameOnly: string; } + array.asMutable().map((value: User) => ({ firstNameOnly: value.firstName })); + const map: Immutable.Immutable = array.map((value: User) => ({ firstNameOnly: value.firstName })); + map.asMutable(); + + // filter. Call the mutable array's 'filter' with the same function to ensure compatability. Make sure the output array is immutable. + array.asMutable().filter((value: User) => value.firstName === 'test'); + const filter: Immutable.Immutable = array.filter((value: User) => value.firstName === 'test'); + filter.asMutable(); + + // slice. Call the mutable array's 'slice' with the same args to ensure compatability. Make sure the output array is immutable. + array.asMutable().slice(); + const slice1: Immutable.Immutable = array.slice(); + slice1.asMutable(); + array.asMutable().slice(1); + const slice2: Immutable.Immutable = array.slice(1); + slice2.asMutable(); + array.asMutable().slice(1, 2); + const slice3: Immutable.Immutable = array.slice(1, 2); + slice3.asMutable(); + array.asMutable().slice(undefined, 2); + const slice4: Immutable.Immutable = array.slice(undefined, 2); + 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' }); + const concat: Immutable.Immutable = array.concat({ firstName: 'Happy', lastName: 'Cat' }); + concat.asMutable(); + + // reduce. Call the mutable array's 'reduce' with the same function to ensure compatability. Make sure the output array is immutable. + array.asMutable().reduce((previous, current) => ({ ...previous, lastName: current.lastName })); + const reduce1: Immutable.Immutable = array.reduce((previous, current) => ({ ...previous, lastName: current.lastName })); + reduce1.asMutable(); + // NOTE: this is effectively a map function + array.asMutable().reduce((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []); + const reduce2: Immutable.Immutable = array.reduce((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []); + reduce2.asMutable(); + + // reduceRight. Call the mutable array's 'reduceRight' with the same function to ensure compatability. Make sure the output array is immutable. + array.asMutable().reduceRight((previous, current) => ({ ...previous, lastName: current.lastName })); + const reduceRight1: Immutable.Immutable = array.reduceRight((previous, current) => ({ ...previous, lastName: current.lastName })); + reduceRight1.asMutable(); + // NOTE: this is effectively a map function + array.asMutable().reduceRight((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []); + const reduceRight2: Immutable.Immutable = array.reduceRight((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []); + reduceRight2.asMutable(); + // asMutable const mutableArray1: User[] = array.asMutable(); const mutableArray2: User[] = array.asMutable({ deep: true }); @@ -141,3 +224,21 @@ interface ExtendedUser extends User { const replacedUser01 = immutableUser.replace({ firstName: 'Super', lastName: 'Monkey' }); const replacedUser02 = immutableUser.replace({ firstName: 'Super', lastName: 'Monkey' }, { deep: true }); } + +// +// Instance syntax: immutable date +// --------------------------------------------------------------- + +{ + // ImmutableDate cannot access mutable methods like setDate, etc, but CAN access getDate(). + // Once we make it mutable (i.e, a regular Date), we can use those methods + const immutableDate: Immutable.ImmutableDate = Immutable.from(new Date()); + // immutableDate.setDate(1); + immutableDate.getDate(); + immutableDate.asMutable().setDate(1); + + const immutableDate2: Immutable.ImmutableDate = Immutable(new Date()); + // immutableDate2.setDate(1) + immutableDate2.getDate(); + immutableDate2.asMutable().setDate(1); +}