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
This commit is contained in:
Paul Huynh
2019-01-14 09:59:52 +11:00
parent efb66b22be
commit ba96b0832c
2 changed files with 170 additions and 8 deletions
+69 -8
View File
@@ -4,12 +4,16 @@
// Stepan Burguchev <https://github.com/xsburg>
// Geir Sagberg <https://github.com/geirsagberg>
// Richard Honor <https://github.com/RMHonor>
// Paul Huynh <https://github.com/pheromonez>
// 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<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
@@ -84,18 +88,75 @@ declare namespace SeamlessImmutable {
replace<S>(valueObj: S, options?: ReplaceConfig): Immutable<S>;
}
type ImmutableObject<T> = ImmutableObjectMixin<T> & { readonly [P in keyof T]: Immutable<T[P]> };
interface ImmutableArrayMixin<T extends Array<T[0]>> {
asMutable(opts?: AsMutableOptions): T;
asObject(toKeyValue: (item: T[0]) => [string, any]): Immutable<object>;
flatMap<TTarget>(mapFunction: (item: T[0]) => TTarget): Immutable<TTarget extends any[] ? TTarget : TTarget[]>;
/** An ImmutableArray provides read-only access to the array elements, and provides functions (such as `map()`) that return immutable data structures. */
type ImmutableArray<T> = Readonly<ImmutableArray.Remaining<T> & ImmutableArray.Additions<T> & ImmutableArray.Overrides<T>>;
namespace ImmutableArray {
/** New methods added by seamless-immutable. */
interface Additions<T> {
asMutable(opts?: AsMutableOptions): T[];
asObject(toKeyValue: (item: T) => [string, any]): Immutable<object>;
flatMap<TTarget>(mapFunction: (item: T) => TTarget): Immutable<TTarget extends any[] ? TTarget : TTarget[]>;
}
/** Custom implementation of the array functions, which return Immutable. */
interface Overrides<T> {
map<TTarget>(mapFuction: (item: T) => TTarget): Immutable<TTarget[]>;
filter(filterFunction: (item: T) => boolean): Immutable<T[]>;
slice(start?: number, end?: number): Immutable<T[]>;
concat(...arr: T[]): Immutable<T[]>;
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): Immutable<T>;
reduce<TTarget>(callbackfn: (previousValue: TTarget, currentValue: T, currentIndex: number, array: T[]) => TTarget, initialValue?: TTarget): Immutable<TTarget>;
reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): Immutable<T>;
reduceRight<TTarget>(callbackfn: (previousValue: TTarget, currentValue: T, currentIndex: number, array: T[]) => TTarget, initialValue?: TTarget): Immutable<TTarget>;
}
/** These methods are banned by seamless-immutable. */
type MutatingArrayMethods = Extract<keyof any[], 'push' | 'pop' | 'sort' | 'splice' | 'shift' | 'unshift' | 'reverse'>;
/** NOTE: These methods mutate data, but seamless-immutable does not ban them. We will ban them in our type definitions. */
type AdditionalMutatingArrayMethods = Extract<keyof any[], 'copyWithin' | 'fill'>;
/** The remaining properties on Array<T>, after we remove the mutating functions and the wrapped non-mutating functions. */
type Remaining<T> = Omit<T[], MutatingArrayMethods | AdditionalMutatingArrayMethods | keyof Overrides<any>>;
}
type BaseImmutable<T> = T extends any[] ? ImmutableArrayMixin<T> : ImmutableObjectMixin<T>;
/** 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<T> = {
readonly [P in keyof T]: T[P] extends object ? Immutable<T[P]> : T[P];
} & BaseImmutable<T>;
// These methods are banned by seamless-immutable
type MutatingDateMethods = Extract<keyof Date, 'setDate' | 'setFullYear' | 'setHours' | 'setMilliseconds' | 'setMinutes' | 'setMonth' | 'setSeconds' |
'setTime' | 'setUTCDate' | 'setUTCFullYear' | 'setUTCHours' | 'setUTCMilliseconds' | 'setUTCMinutes' |
'setUTCMonth' | 'setUTCSeconds' | 'setYear'>;
/** Only allows Date methods, which are the getters. */
type Remaining = Omit<Date, MutatingDateMethods>;
}
type Immutable<T, O extends object = {}> =
T extends Promise<infer U> ? Promise<Immutable.MakeImmutable<U, O>> :
Immutable.MakeImmutable<T, O>;
namespace Immutable {
type AnyFunction = (...args: any[]) => any;
type AlreadyImmutable<O extends object = {}> = ImmutableObject<O> | ImmutableArray<any> | ImmutableDate;
type Primitive = boolean | number | string | symbol | AnyFunction | undefined | null;
type CannotMakeImmutable<O extends object = {}> = AlreadyImmutable<O> | Primitive;
type MakeImmutable<T, O extends object = {}> =
T extends CannotMakeImmutable<O> ? T :
T extends Array<infer Element> ? ImmutableArray<Element> :
T extends Date ? ImmutableDate :
ImmutableObject<T>;
}
function from<T>(obj: T, options?: Options): Immutable<T>;
@@ -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<User>(resolve => resolve({
firstName: 'Angry',
lastName: 'Monkey',
}));
const immutablePromise = Immutable(promise);
immutablePromise.then((user: Immutable.Immutable<User>) => user.asMutable());
// Construction with Immutable() multiple times only creates an Immutable once
const user3: Immutable.Immutable<User> = 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<User[]> = 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<FirstName[]> = 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<User[]> = 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<User[]> = array.slice();
slice1.asMutable();
array.asMutable().slice(1);
const slice2: Immutable.Immutable<User[]> = array.slice(1);
slice2.asMutable();
array.asMutable().slice(1, 2);
const slice3: Immutable.Immutable<User[]> = array.slice(1, 2);
slice3.asMutable();
array.asMutable().slice(undefined, 2);
const slice4: Immutable.Immutable<User[]> = 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<User[]> = 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<User> = array.reduce((previous, current) => ({ ...previous, lastName: current.lastName }));
reduce1.asMutable();
// NOTE: this is effectively a map function
array.asMutable().reduce<FirstName[]>((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []);
const reduce2: Immutable.Immutable<FirstName[]> = array.reduce<FirstName[]>((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<User> = array.reduceRight((previous, current) => ({ ...previous, lastName: current.lastName }));
reduceRight1.asMutable();
// NOTE: this is effectively a map function
array.asMutable().reduceRight<FirstName[]>((previous, current) => previous.concat({ firstNameOnly: current.firstName }), []);
const reduceRight2: Immutable.Immutable<FirstName[]> = array.reduceRight<FirstName[]>((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);
}