Skip to content

Instantly share code, notes, and snippets.

@KilianKilmister
Last active August 6, 2021 01:15
Show Gist options
  • Save KilianKilmister/3dd428fc9173673839aadb84c10c1be5 to your computer and use it in GitHub Desktop.
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`.
// 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