1

TypeScript provides the ability to use MappedTypes to create a getter interface from a record type.

For example as per the docs:

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;

The LazyPerson becomes equivalent to:

type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}

I would like do something similar to Getters but instead produce an overloaded function signature type that will disambiguate the return type based on the literal key string.

function get(k: 'name'): string;
function get(k: 'age'): number;
function get(k: 'location'): string;
function get(k) {
  // do the work
}

It seems like it should be possible given the above type magic.

However I cannot find any information as to how to do this.

CMCDragonkai
  • 6,222
  • 12
  • 56
  • 98

1 Answers1

1
type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
  name: string;
  age: number;
  location: string;
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type Values<T> = T[keyof T]

type AllowedStates<T> = {
  [Prop in keyof T]: (arg: Prop) => T[Prop]
}

type Overloading<T> = UnionToIntersection<Values<AllowedStates<T>>>

const get: Overloading<Person> = (arg: string) => {
  return null as any
}

const result = get('age') // number
const result2 = get('name') // string
const result3 = get('location') // string

Playground

YOu need to dynamicly create all allowed states - see AllowedStates

Then get all Values from AllowedStates

Then convert union Values to intersection - UnionToIntersection. Because intersection of functions produces function overloading.

UPDATE

Is there a way to use that type on a class method and not an arrow function?

Yes, you don't event need to make any overloads

interface Person {
  name: string;
  age: number;
  location: string;
}

const person = {
  name: 'string',
  age: 42,
  location: 'string',
}

class Overloads<T> {
  constructor(public obj: T) { }
  get<Key extends keyof T>(key: Key): T[Key] {
    return this.obj[key]
  }
}

const get = <T,>(obj: T) => <Key extends keyof T>(arg: Key): T[Key] => obj[arg]
{
  const getter = get(person);
  const x = getter('age') // number
  const xx = getter('name')  // string
}

const _ = new Overloads(person);
const __ = _.get('age') // number
const ___ = _.get('name') // string

Playground

SYntax for method overloading:


class Overloads {
  get(key: 'hello'): string
  get(key: 'bye'): number
  get(key: string) {
    return null as any
  }
}

UPDATE

type Person = {
  name: string;
  age: number;
  location: string;
};

class Overloads {
  get<Key extends keyof Person>(key: Key): Person[Key]
  get<Key extends keyof Person>(key: Key) {
    switch (key) {
      case 'name': {
        return 'string';
      }
      case 'age': {
        return 42
      }
      case 'location': {
        return 'string';
      }
      default: {
        return null
      }
    }

  }
}

const _ = new Overloads();
const __ = _.get('age') // number
const ___ = _.get('name') // string

UPDATE 2

type Person = {
  name: string;
  age: number;
  location: string;
  date: Date;
};

type Values<T> = T[keyof T];

type Union<T> = Values<{
  [Prop in keyof Person]: [Prop, Person[Prop]]
}>

class Overloads {
  set(...args: Union<Person>): void {
    if (args[0] === 'date') {
      console.log(args[1].getTime());
    }
  }
}
  • Is there a way to use that type on a class method and not an arrow function? – CMCDragonkai Aug 02 '21 at 10:00
  • 1
    Wow that's awesome. That removes many lines from my code. – CMCDragonkai Aug 02 '21 at 10:21
  • I was trying your solution on this variant, but it doesn't quite work: https://www.typescriptlang.org/play?#code/C4TwDgpgBAChBOBnA9gOygXigbwFBSlQEMBbCALikWHgEtUBzAbnyiIYsIFcSAjBFgQA2yAMZFgtNJWp1GLAL4tcooUUSIoAeQBuCEUQAmmvAQ7AAPAGkIIKBAAewCKmNQA1reQAzWAhSoAHwAFJ4glDYgAJSUcEhoANqRALo4rAS0vqG2mBhYAOTEZPlRaQTlUPAQwFzw6Pmy9Az5guUK9kKI0JlQ2XZ5BewQJWUVldW16AAsAEytBO0Qnd1ZYbkFIuKSaCOmY1U1dVANNE0t6VAKrFdXKmjUUAD6mIQQAO7aevAGxsFRLKJ7sAns8sI8AHTmYL5IYjAD0cO4fAQd1QD0eGJeEKhhVIw1KCKop0YQA – CMCDragonkai Aug 02 '21 at 13:32
  • I have a situation where I need to perform a side effect actually fetch the data. So that's why I'm using if/else. I could also use a switch case. – CMCDragonkai Aug 02 '21 at 13:33
  • Made update. In this case you need to add overload, because you did not provide object of PErson interface – captain-yossarian from Ukraine Aug 02 '21 at 13:39
  • Awesome that allowed me to make some progress, but I noticed another problem. That is when you want to match on the value type, like for a setter function, for example `setter(key: Key, value: Person) { ... }`, a particular `value` type is `Date`, and I wasn't able to narrow the `value` type to `Date`. Seems like a limitation of TypeScript here... – CMCDragonkai Aug 02 '21 at 14:08
  • @CMCDragonkai could you please provide an example? – captain-yossarian from Ukraine Aug 02 '21 at 14:11
  • This works from the outside, but not from the inside: https://www.typescriptlang.org/play?ts=4.4.0-beta&ssl=14&ssc=2&pln=1&pc=1#code/C4TwDgpgBAChBOBnA9gOygXigbwFBSlQEMBbCALikWHgEtUBzAbnyiIYsIFcSAjBFgQA2yAMZFgtNJWp1GgqABMJnACIqWAXxa5RQookRQA8gDcEIooqN4CiCMAA8AaQggoEAB7AIqa1ABrN2QAM1gEFFQAPgAKIJBKVxAAGihTIiEuTjgkNABtJIBdAEpKU2RaRRxWAlowuLdMDCwAcmUfFuLqgh6oUTQUIQgAOhEGGPTMkY5gABVaMhji4oUCTVZ1zSA – CMCDragonkai Aug 02 '21 at 14:13
  • Made an update. Please keep in mind, internal code knows nothing about overloadings. If `key` and `value` are not part of one data structure, TS can't figure out that if `key` is `name` - `value` should be a string. TS will figure it out only if these argument will be a part of one data structure – captain-yossarian from Ukraine Aug 02 '21 at 14:25
  • I see I won't be able to make use of your suggestion because I'm fetching the data from the database. – CMCDragonkai Aug 02 '21 at 14:29
  • 1
    But it works well enough. I'm just using `@ts-ignore` for now – CMCDragonkai Aug 02 '21 at 14:29