1
votes

I'm trying to define a method that operates as a getter, taking an optional parameter. The getter provides access to an object of type T, and should return either the entire object, or a property on that object.

The challenge is that I am trying to defined the method in two places, first in an interface, and second in the actual implementation.

Here's my approach:


// Getter defines both overloads
interface StoreGetter {
    <T>(): T;
    <T, K extends keyof T>(prop: K): T[K];
}

// Store has a generic type, and exposes that type and properties on that type
interface Store<T> {
    get: StoreGetter;

    // Either one works individually
    // get: <T>() => T;
    // get: <T, K extends keyof T>(prop: K) => T[K];
}

export function makeStore<T>(initial: T): Store<T> {
    let value: T = initial;

    // Apparently, you can only define overloads via a function declaration
    // function get<T>(): T;
    // function get<T, K extends keyof T>(prop: K): T[K];
    function get(prop?: keyof T) {
        if (typeof prop !== 'undefined') {
            return value[prop];
        }
        return value;
    }

    return {
        get,
    };
}

const store = makeStore({
    text: '',
    items: [],
    num: 1
});

// Argument of type '"text"' is not assignable to parameter of type 'never'.(2345):
store.get('text') 

// Object is of type 'unknown'.(2571)
store.get().

Unfortunately, the two definitions seem to clobber each other.

How can I define this method with overloads, and have correct type inference for both calls?

1

1 Answers

2
votes

After many failed attempts, I've discovered one configuration that produces the expected inferences:

interface StoreGetter<T> {
    (): T;
    <K extends keyof T>(props: K): T[K];
}

interface Store<T> {
    get: StoreGetter<T>;
    set: (val: any | T) => void;
}

export function makeStore<T>(initial: T): Store<T> {
    let value: T = initial;
    let listeners: Function[] = [];

    function get(): T;
    function get<K extends keyof T>(prop: K): T[K];

    function get(prop?: keyof T): T | T[keyof T] {
        if (typeof prop !== 'undefined') {
            return value[prop];
        }
        return value;
    }

    return {
        get,
        set: (val: any) => {
            value = {
                ...value,
                ...val,
            };
            listeners.forEach(fn => fn(value));
        }
    };
}

const store = makeStore({
    text: '',
    items: [],
    num: 1
});

// Both work with type inference
store.get('text').toUpperCase
store.get().items

Still hoping to find a way to do it with an inline/anonymous function.

On a positive note, this approach works seamlessly in a declarations file (e.g., store.d.ts), enabling the use of a single declaration:

interface StoreGetter<T> {
  (): T;
  <K extends keyof T>(props: K): T[K];
}

interface Store<T> {
  get: StoreGetter<T>;
}

export function makeStore<T>(initial: T): Store<T>;

export function useStore<T>(store: T, prop?: string): [T|any, (newState: T|any) => void];

And then in a separate JS file:

const store = makeStore({
  keypresses: 0,
  text: '',
  arrows: [],
});

// Both inferred:
store.get('keypresses').toFixed
store.get().arrows.push

This produces the expected annotations in VS code:

VS Code showing key suggestion

VS Code suggestion for no parameter overload

VS Code suggestion for key overload