Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lookup type within object in TypeScript

Tags:

typescript

I'm trying to introduce a lookup type within an object. Let's say my object looks like

class PersonList {
  persons = {
    john: 'description of john',
    bob: 'description of bob'
  }
}

I'd like to have a getter to get a person from persons but without specifying the the persons object.

The getProperty from the docs

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];  // Inferred type is T[K]
}

Wants to have an obj which I want to get rid of in my getter. I've tried proxying the getter, but that didn't work out:

class PersonList {
  persons = {
    john: 'description of john',
    bob: 'description of bob'
  };

  getPerson(name) {
    return this.getProperty(this.persons, name);
  }

  private getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];  // Inferred type is T[K]
  }
}

This sadly doesn't throw an error when trying to do something like personList.getPerson('someonenotincluded'); - and additionally autocomplete doesn't work either.

like image 853
nehalist Avatar asked Sep 06 '25 06:09

nehalist


1 Answers

I'd take that inline type and name it (but keep reading, you don't have to):

interface Persons {
  john: string;
  bob: string;
}

Then you can use keyof Persons as the parameter type in getPerson:

class PersonList {
  persons: Persons = {
    john: 'description of john',
    bob: 'description of bob'
  };

  getPerson(name: keyof Persons) {
    return this.persons[name];
  }
}

So then if pl is a PersonList:

console.log(pl.getPerson('john')); // Works
console.log(pl.getPerson('someonenotincluded')); // Error

Live on the playground.

But, if you prefer to keep it inline, you can by using keyof PersonList['persons'] as the parameter type:

class PersonList {
  persons = {
    john: 'description of john',
    bob: 'description of bob'
  };

  getPerson(name: keyof PersonList['persons']) {
    return this.persons[name];
  }
}

Live on the playground.


In a comment you've asked:

is it possible to implement this in an abstract class? ... it would be awesome to implement the getter in the abstract class, but I haven't found a solution so far.

...with a link to this code template:

abstract class AbstractPersonList {
  protected abstract persons: { [name: string]: string };
}

class Persons extends AbstractPersonList {
  persons = {
    john: 'this is john',
  }
}

class MorePersons extends AbstractPersonList {
  persons = {
    bob: 'this is bob',
  }
}

You can parameterize AbstractPersonList:

abstract class AbstractPersonList<T extends {[name: string]: string}> {
// ------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  protected abstract persons: T;
  public getPerson(name: keyof T): string {
    return this.persons[name];
  }
}

Then you'd have:

class Persons extends AbstractPersonList<{john: string}> {
// -------------------------------------^^^^^^^^^^^^^^^^
  persons = {
    john: 'this is john',
  }
}

class MorePersons extends AbstractPersonList<{bob: string}> {
// -----------------------------------------^^^^^^^^^^^^^^^
  persons = {
    bob: 'this is bob',
  }
}

Which leads to these results, which I think are what you're looking for:

let n = Math.random() < 0.5 ? 'john' : 'bob';

const p = new Persons();
console.log(p.getPerson('john'));  // Works
console.log(p.getPerson('bob'));   // FAILS: Argument of type '"bob"' is not assignable to parameter of type '"john"'.
console.log(p.getPerson(n));       // FAILS: Argument of type 'string' is not assignable to parameter of type '"john"'.

const mp = new MorePersons();
console.log(mp.getPerson('john')); // FAILS: Argument of type '"john"' is not assignable to parameter of type '"bob"'.
console.log(mp.getPerson('bob'));  // Works
console.log(mp.getPerson(n));      // FAILS: Argument of type 'string' is not assignable to parameter of type '"bob"'.

Live on the playground.

like image 88
T.J. Crowder Avatar answered Sep 09 '25 03:09

T.J. Crowder