Last active
August 6, 2021 01:15
-
-
Save KilianKilmister/3dd428fc9173673839aadb84c10c1be5 to your computer and use it in GitHub Desktop.
A TypeScript module exporting a generic function for deriving object via the @@species mechanism and trying to mimic the behaviour of built-in methods which create deriviatives like `Array.prototype.slice`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://stackoverflow.com/questions/62010217/how-to-create-a-new-object-of-same-class-as-current-object-from-within-a-method | |
// Create a new object using the class of an existing object (which might be a | |
// derived class) by trying to mimic the ECMAScript procedure except for the | |
// realm detection or constructor argument validation part. | |
// ## TypeGuards | |
/** Constructor guard */ | |
const isConstructor = (arg: unknown): arg is (new () => object) => | |
typeof arg === 'function' && typeof arg.prototype === 'object' | |
/** Nullish guard */ | |
const isNullish = (arg: any): arg is null | undefined => | |
arg === undefined || arg === null | |
/** Object guard */ | |
const isObject = (arg: any): arg is object => | |
typeof arg === 'function' || (typeof arg === 'object' && arg !== null) | |
// ## Main function | |
/** | |
* Creates a new derived instance of an object which --or any of its super | |
* classes-- implements the `@@species` interface. If `target` is NOT an instance | |
* of `Fallback`, a new instance of `Fallback` will be created instead. Tries to | |
* mimic the behavior of built-in methods which create derived instances; eg. | |
* `Array.prototype.map` or `Promise.prototype.then` | |
* @param Fallback The fallback constructor to use -- usually a *(potential)* | |
* super class. | |
* @param target The object to derive from. | |
* @param args The constructor arguments to pass to the deriving class. | |
* @return The newly derived objects. | |
* @example | |
* class MyMap<K, V> extends Map<K, V> { | |
* map <U>(ƒ: (value: V, key: K, self: this) => U): MyMap<K, U> { | |
* return speciesCreate(Map, this, makeIterableWithMappedValues(ƒ, this) ) | |
* } | |
* } | |
*/ | |
export function speciesCreate <T extends object, Arguments extends any[] = any[]> (Fallback: new (...args: Arguments) => T, target: any, ...args: Arguments): T | |
/** | |
* Returns a function for creating derived instances of a objects which (or any | |
* of its super classes) implements the `@@species` interface. If `target` is | |
* NOT an instance of `Fallback`, a new instance of `Fallback` will be created | |
* instead. Tries to | |
* mimic the behavior of built-in methods which create derived instances; eg. | |
* `Array.prototype.map` or `Promise.prototype.then` | |
* @param Fallback The fallback constructor to use -- usually a *(potential)* | |
* super class. | |
* @return A function for creating derived objects. | |
* @example | |
* const mapSpeciesCreate = speciesCreate(Map) | |
* class MyMap<K, V> extends Map<K, V> { | |
* map <U>(ƒ: (value: V, key: K, self: this) => U): MyMap<K, U> { | |
* return mapSpeciesCreate(this, makeIterableWithMappedValues(ƒ, this) ) | |
* } | |
* } | |
*/ | |
export function speciesCreate <T extends object, Arguments extends any[] = any[]> (Fallback: new (...args: Arguments) => T): (target: any, ...args: Arguments) => T | |
/** | |
* Returns a function for creating new instances of an object who's constructor | |
* (or any of its super classes) implements the `@@species` interface without | |
* any specific fallback constructor. | |
* @return A function for creating derived objects which tries to infer what | |
* constructor to use as a fallback. | |
* @example | |
* const genericSpeciesCreate = speciesCreate() | |
* class MyMap<K, V> extends Map<K, V> { | |
* map <U>(ƒ: (value: V, key: K, self: this) => U): MyMap<K, U> { | |
* return mapSpeciesCreate(this, makeIterableWithMappedValues(ƒ, this) ) | |
* } | |
* } | |
*/ | |
export function speciesCreate <T extends object, Arguments extends any[] = any[]> (): (target: any, ...args: Arguments) => T | |
export function speciesCreate <T extends object, Arguments extends any[] = any[]> (Fallback?: new (...args: Arguments) => T, ...args: [target: any, ...args: Arguments] | []): any { | |
// pre-check if `Fallback` is a constructor if the argument was provided | |
if (Fallback !== undefined && !isConstructor(Fallback)) throw new TypeError('`Fallback` must be a constructor function') | |
// Inner core function for automatic partial application. | |
function speciesCreate (target: any, ...args: Arguments) { | |
// if `Fallback` wasn't provided | |
if (!Fallback && !isConstructor(Fallback ??= target?.constructor)) throw new Error('Cannot automatically infer from `target` what fallback to use for `@@species`.') | |
if (target instanceof Fallback) { | |
let C = target.constructor as unknown | |
if (isObject(C)) C = C[Symbol.species] | |
if (isConstructor(C)) return Reflect.construct(C, args) as T | |
if (!isNullish(C)) throw new TypeError('Invalid `target` argument for `@@species` use.') | |
} | |
return new Fallback(...args) | |
} | |
return args.length ? speciesCreate(...args as Parameters<typeof speciesCreate>) : speciesCreate | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment