dagda1
dagda1

Reputation: 28770

is it possible to separate and reuse multiple function overloads in typescript

I have created this codesanbox

I have 2 functions called slice in 2 classes. Both have the same set of horrible overrieds:

export class Atom<S>  {
  constructor(private initial: S) {
  }
  

  slice<Key extends keyof S>(key: Key): Slice<S[Key], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1]>(key1: Key1, key2: Key2): Slice<S[Key1][Key2], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2]>(key1: Key1, key2: Key2, key3: Key3): Slice<S[Key1][Key2][Key3], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4): Slice<S[Key1][Key2][Key3][Key4], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5): Slice<S[Key1][Key2][Key3][Key4][Key5], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6, key7: Key7): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6][Key7], S>;
  slice<Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6], Key8 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6][Key7]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6, key7: Key7, key8: Key8): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6][Key7][Key8], S>;
  slice(...keys: string[]): any{
    return new Slice(this, keys);
  }
}

If I ever have to change these horrible overrides, then I will need to do it in 2 places.

Is there any way I can reuse the same function in two different places?

I've tried creating a generic slice function


export function slice<A, S, Key extends keyof A>(atom: A, key: Key): Slice<A[Key], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1]>(atom: A, key1: Key1, key2: Key2): Slice<A[Key1][Key2], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2]>(atom: A, key1: Key1, key2: Key2, key3: Key3): Slice<A[Key1][Key2][Key3], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2], Key4 extends keyof A[Key1][Key2][Key3]>(atom: A, key1: Key1, key2: Key2, key3: Key3, key4: Key4): Slice<A[Key1][Key2][Key3][Key4], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2], Key4 extends keyof A[Key1][Key2][Key3], Key5 extends keyof A[Key1][Key2][Key3][Key4]>(atom: A, key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5): Slice<A[Key1][Key2][Key3][Key4][Key5], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2], Key4 extends keyof A[Key1][Key2][Key3], Key5 extends keyof A[Key1][Key2][Key3][Key4], Key6 extends keyof A[Key1][Key2][Key3][Key4][Key5]>(atom: A, key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key: Key6): Slice<A[Key1][Key2][Key3][Key4][Key5][Key6], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2], Key4 extends keyof A[Key1][Key2][Key3], Key5 extends keyof A[Key1][Key2][Key3][Key4], Key6 extends keyof A[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof A[Key1][Key2][Key3][Key4][Key5][Key6]>(atom: A, key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key: Key6, key7: Key7): Slice<A[Key1][Key2][Key3][Key4][Key5][Key6][Key7], S>;
export function slice<A, S, Key1 extends keyof A, Key2 extends keyof A[Key1], Key3 extends keyof A[Key1][Key2], Key4 extends keyof A[Key1][Key2][Key3], Key5 extends keyof A[Key1][Key2][Key3][Key4], Key6 extends keyof A[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof A[Key1][Key2][Key3][Key4][Key5][Key6], Key8 extends keyof A[Key1][Key2][Key3][Key4][Key5][Key6][Key7]>(atom: A, key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key: Key6, key7: Key7, key8: Key8): Slice<A[Key1][Key2][Key3][Key4][Key5][Key6][Key7][Key8], S>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function slice<A, S>(atom: Atom<A>, ...keys: Key[]): Slice<S, A> {
    return new Slice(atom, keys);
  }

and then calling it like this:

  slice<T>(...keys: Key[]): Slice<T, S> {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return slice(this, keys as any);
  }

But the typing gets lost without the overloads on each function.

Upvotes: 1

Views: 236

Answers (2)

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

At one point I thought I saw someone post a comment about using typescript 4.0 beta as it has variadic tuples, not sure what happened to that comment but that is also a valid option: (playground link)

I took a while crafting this code sample, the NestedIndexList type has a number of quirks due to how it is used as a circular constraint in order to get nice meaningful error messages when ever possible.

/**
 * new single signature for slice function, no overrides needed.
 */
declare function slice<T, K extends [keyof T, ...NestedIndex<T,K>]>(obj: T, ...keys: K): ResolveKeys<T,K>

/**
 * resolves the type of an object from a list of nesting keys.
 */
type ResolveKeys<T, K extends unknown[]> = K extends [] ? T 
    :K extends [infer A, ...infer Rest]
        ? {[kk in A & keyof T]: ResolveKeys<T[kk], Rest>}[A & keyof T]
    : unknown;

/**
 * returns extension on valid keys list
 * this is set to be specifically recursive to mimic the behaviour of something like this:
  | [keyof T, keyof T[K[0]]]
  | [keyof T, keyof T[K[0]], keyof T[K[0]][NonNullable<K[1]>]]
  | [keyof T, keyof T[K[0]], keyof T[K[0]][NonNullable<K[1]>], keyof T[K[0]][NonNullable<K[1]>][NonNullable<K[2]>]]
 * this is obviously not maintainable when hardcoded in that way, so this intends to read the length of arguments given and
 * return a suitable list of valid arguments.
 */
type NestedIndex<T, K extends [keyof T, ...unknown[]]> = 
    // if there are no common keys given the current info then don't have any valid keys.
    keyof T[K[0]] extends never ? []
    // if we end up with a total wild card then stop the list, this can happen if using a union variable and we lose all safety for fields after it if we don't check here.
    : (keyof any) extends keyof T[K[0]] ? [] 
    // otherwise see if we have at least one extra element in our list
    : K extends [K[0], infer A, ...infer R]
        ? [A] extends [keyof T[K[0]]]
            // if A is valid then we can keep going recursively, using a bit of object literal trickery
            //  so typescript doesn't complain we are circularly defined.
            ? {[k in A]: [keyof T[K[0]], ...NestedIndex<T[K[0]], [A, ...R]>]}[A]
            // if an element of the key list isn't a valid key then we use never[] to short circuit a bunch of irrelevent possibilities from being generated in invalid cases.
            : [keyof T[K[0]], ...never[]]
        // if we don't have an extra element then we could choose to extend the list by 1 or not.
        // since this is in the constraint I don't think this is noticable ever, but still in theory it's the right way to put it.
        : [] | [keyof T[K[0]]]


// ######################### FOR TESTING ###########################

interface Data {
    a: Data;
    b: {
        foo: {
            c: string
            d: string
        },
        bar:{
            c: number
        }
    }
}
declare const x: Data;
declare const field: "bar" | "foo"
const a = slice(x, "a", "a", "b", field, "d", "")

The key here is that the Keys list generic is constrained to a type defined in terms of earlier elements in the list. Variadic tuples makes this a little nicer but it is also possible for a working implementation although the equivalent definition of NestedIndex isn't nearly as pretty (playground link)

type _N<T> = NonNullable<T>
type KeyArr<T,K extends [keyof T]
    | [keyof T, keyof T[K[0]]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>][_N<K[7]>]]
> = [keyof T]
    | [keyof T, keyof T[K[0]]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>]]
    | [keyof T, keyof T[K[0]], keyof T[K[0]][_N<K[1]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>], 
        keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>], keyof T[K[0]][_N<K[1]>][_N<K[2]>][_N<K[3]>][_N<K[4]>][_N<K[5]>][_N<K[6]>][_N<K[7]>]]


interface _ArrSizes {
    1: [any]
    2: [any,any]
    3: [any,any,any]
    4: [any,any,any,any]
    5: [any,any,any,any,any]
    6: [any,any,any,any,any,any]
    7: [any,any,any,any,any,any,any]
    8: [any,any,any,any,any,any,any,any]
}
type ResolveKeys<T,K extends KeyArr<T,K>> =  
      K extends _ArrSizes[1] ? T[K[0]]
    : K extends _ArrSizes[2] ? T[K[0]][K[1]]
    : K extends _ArrSizes[3] ? T[K[0]][K[1]][K[2]]
    : K extends _ArrSizes[4] ? T[K[0]][K[1]][K[2]][K[3]]
    : K extends _ArrSizes[5] ? T[K[0]][K[1]][K[2]][K[3]][K[4]]
    : K extends _ArrSizes[6] ? T[K[0]][K[1]][K[2]][K[3]][K[4]][K[5]]
    : K extends _ArrSizes[7] ? T[K[0]][K[1]][K[2]][K[3]][K[4]][K[5]][K[6]]
    : K extends _ArrSizes[8] ? T[K[0]][K[1]][K[2]][K[3]][K[4]][K[5]][K[6]][K[7]]
    : unknown;

/// #### TESTING #####
interface Data {
    foo: number
    bar: {
        baz: {
            thing: string
        }
    }
}
declare const x: Data

// this is the new signature, using KeyArr<T,K> as constraint makes this very easy to replicate.
declare function slice<T,K extends KeyArr<T,K>>(obj: T, ...keys: K): ResolveKeys<T,K>;

const y = slice(x, "bar", "baz", "thing")

Upvotes: 1

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

I thought there was a simple way using interface merging but I was unable to get it working, one I was able to get working is to use arrow function property instead of a proper method since this gives you a place to declare a function type with many overloads:

type terribleSliceSignature<S> = {
    <Key extends keyof S>(key: Key): Slice<S[Key], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1]>(key1: Key1, key2: Key2): Slice<S[Key1][Key2], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2]>(key1: Key1, key2: Key2, key3: Key3): Slice<S[Key1][Key2][Key3], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4): Slice<S[Key1][Key2][Key3][Key4], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5): Slice<S[Key1][Key2][Key3][Key4][Key5], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6, key7: Key7): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6][Key7], S>;
    <Key1 extends keyof S, Key2 extends keyof S[Key1], Key3 extends keyof S[Key1][Key2], Key4 extends keyof S[Key1][Key2][Key3], Key5 extends keyof S[Key1][Key2][Key3][Key4], Key6 extends keyof S[Key1][Key2][Key3][Key4][Key5], Key7 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6], Key8 extends keyof S[Key1][Key2][Key3][Key4][Key5][Key6][Key7]>(key1: Key1, key2: Key2, key3: Key3, key4: Key4, key5: Key5, key6: Key6, key7: Key7, key8: Key8): Slice<S[Key1][Key2][Key3][Key4][Key5][Key6][Key7][Key8], S>;
}
export class Atom<S> {
    constructor(private initial: S) {
    }
    slice: terribleSliceSignature<S> = (...keys: string[]): any => {
      return new Slice(this, keys);
    }
}

I know this isn't ideal but it is at least better than listing that overload twice.

Upvotes: 1

Related Questions