Skip to content

Instantly share code, notes, and snippets.

@loilo
Created August 22, 2019 11:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save loilo/bd21570468a7b66d1c7c03d9af03c774 to your computer and use it in GitHub Desktop.
Save loilo/bd21570468a7b66d1c7c03d9af03c774 to your computer and use it in GitHub Desktop.
Some types I found useful during some tricky TypeScript programming
/*
* These are some types I found useful during some tricky TypeScript programming.
* Many of them are not too common or intuitive to come up with, this is why I'm writing them down here for future lookup.
*
* Note: Be careful when copy-pasting, some of these types depend on others.
*/
/**
* Various utilities for working with classes
*/
namespace Class {
/**
* A constructable (non-abstract) class
* - T represents objects created by the class
* - U represents the arguments passed to the class constructor
*/
export type Constructable<T extends {} = {}, U extends any[] = any[]> = new(...args: U) => T;
/**
* An abstract (non-constructable) class
* - T represents objects created by the class
*/
export type Abstract<T extends {} = {}> = Function & { prototype: T }
/**
* Any class, may be constructable or abstract
* - T represents objects created by the class
*/
export type Any<T extends {} = {}> = Abstract<T> | Constructable<T>
/**
* Get the arguments list of a class constructor as a tuple
*/
export type ConstructorArgs<T> = T extends Constructable<any, infer U> ? U : never
/**
* Create a constructable class type from a constructable or abstract class type
*/
export type MakeConstructable<T extends Any> = T extends Constructable
? T
: T extends Abstract<infer V>
? Constructable<V>
: never
/**
* Like InstanceType<T>, but works on abstract classes as well
*/
export type InstanceType<T extends Any> = T extends Constructable<infer U>
? U
: T extends Abstract<infer V>
? V
: never
}
/**
* Merge properties of T and U into a new type
* Property types of U override conflicting types of T
*/
type Merge<T, U> = { [P in keyof T | keyof U]: P extends keyof U ? U[P] : P extends keyof T ? T[P] : never }
/**
* From an object type create a union of object types, each with one property of the object
* Kind of like Partial<T>, but only requires a single property to match and fails on empty objects
*
* Example:
*
* { a: number, b: string, c: boolean }
*
* ->
*
* { a: number } | { b: string } | { c: boolean }
*/
type AnyOverlap<T> = { [P in keyof T]: { [X in P]: T[X] } }[keyof T]
/**
* From an object type T create the type that does not have any conflicts with T
* Basically like Partial<T>, but additionally allows empty intersection
*/
type NoConflict<T> = { [key: string]: any } & Partial<T>
/**
* Like NoConflict<T>, but for class types
*/
type NoConflictClass<T extends Class.Any> = Class.Any<NoConflict<Class.InstanceType<T>>>
/**
* Below is my personal approach to TypeScript mixins.
* It's a slightly altered approach from regular mixin classes (see https://mariusschulz.com/blog/mixin-classes-in-typescript).
*
* Advantages:
* - Built-in support for extending abstract classes (possible with mixin classes but requires annoying amount of additional boilerplate)
* - Type-hinting for super() calls in classes extending mixins
*
* Disadvantages:
* - Additional boilerplate through calling `Mixin()`
* This is however compensated by omitting other boilerplate (manual type-hinting).
*/
namespace Mixin {
/**
* A Function that takes a class T and returns a class U that extends T
*/
export interface Extender<T extends Class.Any, U extends Class.MakeConstructable<T>, V extends any[]> {
(Base: T, ...additionalArgs: V): U
}
/**
* A generic function with a class type T that takes a class type U
* (which must not be conflicting with T) and returns the intersection of T and U
*/
export interface Wrapper<T extends Class.Constructable, V extends any[]> {
<U extends NoConflictClass<T>>(Base: U, ...additionalArgs: V): U & T
}
}
/**
* @param extender A callback which receives a base class T and returns a class U that extends T.
* May take an arbitrary number of additional arguments for more fine-grained control.
* @return A function which can be passed a base class and returns a new class extending it
*
* @example Basic example
* const WriteAccess = Mixin(_ => class extends _ {
* write(file: string, content: string) {
* // Do some write action
* }
* })
*
* class User {
* constructor(protected name: string) {}
* }
*
* class PrivilegedUser extends WriteAccess(User) {
* constructor(name: string, protected role: 'editor' | 'admin') {
* super(name) // <- type-hinted!
* }
*
* method() {
* this.write('/some/file/path', 'some content') // <- type-hinted!
* }
* }
*/
function Mixin<T extends Class.Constructable, U extends Class.MakeConstructable<T>, V extends any[]>(extender: Mixin.Extender<T, U, V>): Mixin.Wrapper<U, V> {
return <X extends NoConflictClass<U>>(Origin: X = class {} as X, ...additionalArgs: V) =>
extender(Origin as any, ...additionalArgs) as X & U
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment