Генерация динамических типов в машинописном тексте

Вот класс:

const COMMANDS = [
    "get-myanimelist-staff-urls",
    "get-myanimelist-anime-explicit-genres",
    "get-myanimelist-anime-genres",
    "get-myanimelist-anime-themes"
    ...
];

class Shiinobi {
    constructor() {
        COMMANDS.forEach((command) => {
            this[command.replaceAll("-", "_")] = async (...args: any[]) => {
                const id = args[0];
                return await this.#spawn({ command, id: id });
            };
        });
    }
    ...

и я использую этот класс Shiinobi например:

const shiinobi = new Shiinobi();
const res = await shiinobi.get_myanimelist_staff_urls();

По сути, это правильный код, поскольку constructor создает такие методы. но для таких методов нет типа:

shiinobi.get_myanimelist_staff_urls()

этот код показывает ts-error

Я пробовал использовать машинописный текст, изготовленный по индивидуальному заказу Utility, но надежды не было.

Редактировать:
вот код репозитория GitHub


63
2

Ответы:

Вы можете сначала объявить некоторые типы:

const COMMANDS = [
    "get-myanimelist-staff-urls",
    "get-myanimelist-anime-explicit-genres",
    "get-myanimelist-anime-genres",
    "get-myanimelist-anime-themes"
    // ...
] as const;

type COMMANDS_TYPE = typeof COMMANDS;

type ReplaceDashWithUnderscore<T extends string> =
  T extends `${infer P1}-${infer R1}` ? `${P1}_${ReplaceDashWithUnderscore<R1>}` :
  T extends `${infer P2}-${infer R2}` ? `${P2}_${ReplaceDashWithUnderscore<R2>}` :
  T extends `${infer P3}-${infer R3}` ? `${P3}_${ReplaceDashWithUnderscore<R3>}` :
  T extends `${infer P4}-${infer R4}` ? `${P4}_${ReplaceDashWithUnderscore<R4>}` :
  T extends `${infer P5}-${infer R5}` ? `${P5}_${ReplaceDashWithUnderscore<R5>}` :
  T;

type Underscore<T extends COMMANDS_TYPE> = {
  [K in keyof T]: T[K] extends string ? ReplaceDashWithUnderscore<T[K]> : never;
};

type ToObj<T extends Underscore<COMMANDS_TYPE>> = {
  // TODO Should Promise<void> be Promise<return type of spawn>
  [K in T[number]]: (id: string, ...args: any[]) => Promise<void>; 
};

Вышеупомянутое позволяет вам определить объект с помощью команд как методов в правильном формате:

Тогда вместо использования класса вы можете использовать фабрику для литерала объекта. С этим будет проще справиться при использовании динамических свойств:

function shinobiFactory() {

  // private stuff
  const somethingPrivate: number = 0;

  interface SpawnConfig {
    command: typeof COMMANDS[number];
    id: string;
  }

  function spawn(config: SpawnConfig) {
    return somethingPrivate;
  }

  function someOtherMethod() {
    return somethingPrivate;
  }

  function underscore<T extends string>(s: T) {
    return s.split("-").join("_") as ReplaceDashWithUnderscore<T>;
  }

  const commandProps = COMMANDS.reduce(
    (prev, curr) => {
      const key = underscore(curr)
      const value = async (id: string, ...args: any[]) => {
        return await spawn({ command: curr, id: id });
      };
      const update = {
        ...prev,
        ...{
          [key]: value
        }
      };
      return update;
    },
    {}
  ) as ToObj<Underscore<COMMANDS_TYPE>>;

  // Return only what you want to be public
  return {
    ...commandProps,
    someOtherMethod
  }
  
}

Наконец, вы можете вызвать метод для создания экземпляров вашего «класса». Как видите, и свойства, и аргументы верны:

Демо-версия игровой площадки


Решено

В итоге я использовал специальную утилиту , которая заменяет - на _ и расширяет интерфейс Shiinobi.

Вот измененный код:

type Command = (typeof COMMANDS)[number];

type ReplaceHyphens<T extends string> = T extends `${infer P1}-${infer P2}`
    ? `${P1}_${ReplaceHyphens<P2>}`
    : T;

type ShiinobiProperties = {
    [K in Command as ReplaceHyphens<K>]: (id?: number) => Promise<any>;
};

interface Shiinobi extends ShiinobiProperties {}

А вот пример минимального воспроизведения


Интересные вопросы для изучения