Skip to content

Instantly share code, notes, and snippets.

@jspears
Created August 22, 2022 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jspears/aee3d7a61b731e01f4ac9aad503f4aea to your computer and use it in GitHub Desktop.
Save jspears/aee3d7a61b731e01f4ac9aad503f4aea to your computer and use it in GitHub Desktop.
Make an parameter object work with 'named' parameters

Some typescript magic to make named arguments a reality... Sort of.

type Fn = (arg: any) => any;

// oh boy don't do this
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never

// TS4.1+
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = true extends N ? [] : [...TuplifyUnion<Exclude<T, L>>, L]


/**
 * Sometimes you want to be able to take named parameters,
 * and or 
 * 
 * Should put optional parameters last.
 */
type Never<T> = { [K in keyof T as T[K] extends never ? never : K extends 'prototype' ? never : K]: T[K] }

type WithRequiredKeys<T> = Never<{ [K in keyof T]-?: {} extends Pick<T, K> ? never : [K, T[K]] }>;
type WithOptionalKeys<T> = Never<{ [K in keyof T]-?: {} extends Pick<T, K> ? [K, T[K]] : never }>;
type NamedParameters_<T> = TuplifyUnion<T[keyof T]>
type NamedParameters<T> = [...NamedParameters_<WithRequiredKeys<T>>, ...NamedParameters_<WithOptionalKeys<T>>];

type FlattenName<T> = T extends [[infer Name, infer Type], ...infer Rest] ? [Name, Type, ...FlattenName<Rest>] : [];
type FlattenType<T> = T extends [[any, infer Type], ...infer Rest] ? [Type, ...FlattenType<Rest>] : [];

type Optional<T> = T extends [ infer K, infer V, ...infer Rest] ? ([K, V, ...Optional<Rest>] | Optional<Rest>) : [];

type AllArgs<T> = [T] |[...FlattenName<NamedParameters_<WithRequiredKeys<T>>>, ...Optional<FlattenName<NamedParameters_<WithOptionalKeys<T>>>>];

type F = AllArgs<Args>;

function otherIsString(all: any[]) {
    const ret = {} as Record<string, unknown>;
    for (let i = 0; i < all.length; i += 2) {
        if (typeof all[i] !== 'string') {
            return;
        } else {
            ret[all[i]] = all[i + 1];
        }
    }
    return ret;
}

function args<T extends Fn>(fn: T): (...a: AllArgs<Parameters<T>[0]>) => ReturnType<T> {
    return (...all: AllArgs<Parameters<T>[0]>): ReturnType<T> => {
        if (all.length === 1) {
            return fn.call(null, all[0]);
        }
        let obj = otherIsString(all)
        if (!obj) {
            throw new Error('not a valid call signature');
        }
        return fn.call(null, obj)
    }
}

type Args = { hello?: string, goodbye: number, name: string }

function myFunc(arg: Args) {

    return true;
}

const f2 = args(myFunc);

f2('goodbye', 1, 'name', 'joe', 'hello', 'doe')
f2({ goodbye: 1, name:'Joe' })

class Stuff {
    foo = args((v:Args)=>{
        console.log('helo ', v.name);
    })
}
var s = new Stuff();
s.foo( 'goodbye', 1, 'name', 'John');
s.foo( 'goodbye', 1, 'name', 'John', 'hello', 'world');
s.foo({goodbye:1, name:'John', hello:'World'})

Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment