DEV Community

Discussion on: TypeScript is wasting my time

Collapse
peerreynders profile image
peerreynders • Edited on

Types you don't care about are unknown

export default (_: unknown, inject: Function) => {
  inject('myPlugin', /* Plugin code */);
};
Enter fullscreen mode Exit fullscreen mode

Use typeof type operator and ReturnType to generate the type for you.

function makeAPI(inject: Function) {
  return {
    get(key: string): object {
      /* Code code code */
      return {}; 
    },
    set(key: string, value: object): void {
      /* Code code code */
    }
  }
}

export type API = ReturnType<typeof makeAPI>;

export default (_: unknown, inject: Function) => {
  inject('myPlugin', makeAPI(inject));
};
Enter fullscreen mode Exit fullscreen mode

Other than that it's just the "cost of doing business". Those interfaces had no idea that you would be adding your plugin. So now you are merging your plugin into those existing interfaces.

Also it's not duplication. There is type declaration space and variable declaration space or as I like to put it:

  • type space: where TypeScript compile time types live
  • value space: where JavaScript runtime values live

The makeAPI function is pure JavaScript and lives in value space. With the help typeof and ReturnType we pulled API into type space for compile time type checking. Monkey patching those interfaces happens at runtime in value space, so TypeScript can't really track where it is going to end up - yet value space code is going to try to access it in those places so it becomes necessary to tell TypeScript where it is going to show up.


const ENTITIES = ['users', 'articles'] as const;
type Entities = typeof ENTITIES[number]; // "users" | "articles"

type BuilderStep = (this: URLBuilder, id?: string) => URLBuilder;
type URLBuilder = {
  currentUser: (this: URLBuilder) => URLBuilder;
  toString: () => string;
} & Record<Entities, BuilderStep>;

function API() {
  let url = 'api';
  const builder: Partial<URLBuilder> = {
    currentUser() {
      return this.users('4321');
    },
    toString() {
      return url;
    },
  };

  ENTITIES.forEach((entity) => {
    const fn: BuilderStep = function (id) {
      const idSegment = id ? `/${id}` : '';
      url += `/${entity}${idSegment}`;
      return this;
    };

    builder[entity] = fn;
  });

  return builder as URLBuilder;
}

// Usage
console.log(`${API().users('4321').articles()}`); // => 'api/users/4321/articles'
Enter fullscreen mode Exit fullscreen mode

TypeScript is a compile time static type checker. Here you are assembling an object dynamically at runtime (in value space). TypeScript hates that - so you have to take TypeScript by the hand and explain to it like it's five.

The big help here is Partial<URLBuilder> because it makes all the members of the object optional so things can be added piece by piece. However for methods we have to assert that this will be a full blown URLBuilder by the time the method runs.

In the end you as the developer have to take the responsibility of asserting that you are sure that builder is no longer Partial but a fullblown URLBuilder.

Collapse
dvddpl profile image
Davide de Paolis

Wow, wow, wow. Lots of useful advanced stuff here 🤩

Collapse
andrewbaisden profile image
Andrew Baisden

Super useful comment.

Collapse
shalvah profile image
Shalvah

Love this reply.

  • Did not blame the dev or castigate him for "being ignorant".
  • Acknowledged that said tool is not perfect and needs to be handled a certain way
  • Provided solutions for the dev's problems
  • Provided links and explanations for those solutions

Top notch.👏👏👏