Использовать значение аргумента в качестве ключа типа в файле объявления

Я работаю над созданием расширения Prisma. По определенным причинам я начал с JavaScript, используя подсказки JSDoc для набора текста. Расширение на самом деле работает отлично; однако подсказки типов ужасны, что заставило меня поверить, что я делаю что-то не так. Фактически, когда я переключился на транспилятор машинописных текстов, в моих файлах объявлений появились всевозможные ошибки, которые были невидимы при использовании esbuild. Моя самая большая текущая проблема в их устранении заключается в том, что мне нужно использовать значение, предоставленное пользователем, когда исходное расширение создается в качестве ключа в различных других объявлениях типов в моих файлах объявлений.

Например:

//file: index.d.ts
import { MyMethod1Args } from './method_1.d.ts';

// these are mocks; really imported from Prisma
type SomeType = 'id' | 'foo';
interface SomeOtherType {
  foo: string;
  bar: number;
}

export interface RequiredArgs {
  modelName: string;
  idFieldName?: SomeType; // want to use user value of this as key elsewhere
}

// what I want to do
export type IdField = Pick<SomeOtherType, `${idFieldName}`>;
export type IdFieldKey = keyof IdField; // maybe typeof keyof IdField?

export declare function myExtendsion<I extends RequiredArgs>(args: I): {
  Method1(args: MyMethod1Args): string
}

В этой (ретроспективно ошибочной) попытке использовать литерал шаблона я пытаюсь создать определение типа, которое зависит от пользовательского ввода. В коде их несколько десятков.

Причина такого решения, как я уже упоминал, заключается в том, что JavaScript уже работает и уже «набран» с помощью подсказок JSDoc, и я хотел бы перенести его, но медленно — без необходимости переписывать каждый файл JS заранее. Прямо сейчас эти подсказки JSDoc импортируются из других файлов объявлений, поэтому мне нужно иметь возможность использовать эти IdField и IdFieldKey в других местах. Например:

//File: method_1.d.ts
import { IdFieldKey } from '$type/index.d.ts';

// trying for MyMethod1Args = { foo: { in: string[] } },
// where "foo" was the value supplied to `idFieldName`.
export type MyMethod1Args = {
  [K in idFieldKey]: { in: string[] }
};

Примечание. Это может иметь или не иметь отношение к ответу, но я включаю это здесь на тот случай, если это повлияет на ситуацию, и чтобы помочь объяснить, как это на самом деле используется. Что я сделал в JavaScript, так это то, что перед экспортом отдельных методов из основной функции я использую HoF, чтобы обернуть их все и передать аргументы верхнего уровня как configArgs, чтобы каждый метод имел доступ к параметрам (например, idFieldName Но я не включаю это в сигнатуру функции, так что это не сбивает с толку Intellisense или что-то в этом роде.

// File: Method_1.js
/**
 * @param {import('$types/method_1.d.ts').MyMethod1Args} args
 * @returns string
 */
export default function (args, configArgs) {
    console.info(args[configArgs.idFieldName].in.join());
}

@jonrsharpe, ты удалил тег typescript-generics. Это правда, я нигде здесь не использовал слово «дженерики», но разве оно не присуще тому, что я пытаюсь сделать?

philolegein 06.07.2024 16:15

• Если этот вопрос не зависит от Prisma, пожалуйста, отредактируйте код, чтобы он представлял собой минимально воспроизводимый пример , который не использует сторонние типы для демонстрации. Мы должны иметь возможность копировать и вставлять его в наши собственные IDE и работать над ним, не добавляя ничего лишнего. Также используйте общепринятые имена; Имена пользовательских типов должны быть в UpperCamelCase. • Какого типа вы ожидаете idFieldName? В этом примере только K? Если да, то почему бы не использовать K? Если нет, то что это? Кроме того, {[K]:⋯} должно быть {[P in K]:⋯} или Record<K, ⋯>. Но без минимально воспроизводимого примера я просто предполагаю. Поэтому, пожалуйста, отредактируйте, чтобы это обеспечить.

jcalz 06.07.2024 16:16

@jcalz, Готово! Я выложил это на codeandbox. Допустимые значения idFieldName зависят от того, что было предусмотрено для modelName. Надеюсь, минимальный пример прояснит ситуацию.

philolegein 06.07.2024 18:45

Минимальный воспроизводимый пример должен быть в вопросе в виде открытого текста, а не только в виде внешней ссылки. Ссылка на проект IDE — это здорово, но она также должна быть здесь в виде текста, иначе она не засчитывается. Когда я смотрю на эту ссылку, я вижу ошибки, не связанные с тем, что вы спрашиваете, например export type X; без определения и т. д. Можете ли вы убедиться, что присутствуют только те проблемы, о которых вы спрашиваете?

jcalz 06.07.2024 21:23

(см. предыдущий комментарий). Я также не понимаю, как этот пример отвечает на мои вопросы. Когда я смотрю на K extends InitArgs<M> ? K extends undefined ? 'id' : `${idFieldName}` : never;, я растеряюсь. Если K extends InitArgs<M>, то K определенно не является undefined (потому что InitArgs — это интерфейс), поэтому 'id' никогда не достигается. Зачем тогда ты это пишешь? Это не может быть минимальный воспроизводимый пример , потому что он не минимален, и это еще до того, как я доберусь до `${idFieldName}`, чего до сих пор не понимаю. Не могли бы вы предоставить несколько примеров предполагаемых пар ввода-вывода для этого типа? Это зависит от modelName, но как?

jcalz 06.07.2024 21:30

(см. предыдущие 2 комментария). Я думаю, было бы лучше, если бы вы просто написали очень простой пример, который показывает входные данные и предполагаемые выходные данные, а затем кто-то может написать для вас функцию типа, которая делает это, вместо того, чтобы пытаться отлаживать или понимать ваши неудачные попытки, которые могут быть скорее отвлечением, чем помощью к пониманию. Не могли бы вы отредактировать, чтобы сделать это?

jcalz 06.07.2024 21:31

ОК @jcalz; Я попробовал еще раз с еще более минимальным примером и постарался быть как можно более ясным. Надеюсь, это прояснит.

philolegein 07.07.2024 04:23

Суть минимально воспроизводимого примера заключается в том, что нам нужно иметь возможность скопировать и вставить его в наши собственные IDE и начать над ним работать. Моя IDE не знает ни о PrismaModelProps, ни о множестве других вещей, поэтому мне приходится либо попытаться создать это самостоятельно, либо игнорировать это. Если я проигнорирую это и просто посмотрю на «если пользователь позвонит MyExtensions» по номиналу, я получу что-то вроде эта ссылка на игровую площадку . Это достаточно близко к тому, что вы хотите послужить ответом? Если да, то я напишу ответ. Если нет, пожалуйста отредактируйте , чтобы создать настоящий минимально воспроизводимый пример, демонстрирующий то, чего мне не хватает.

jcalz 07.07.2024 12:55

Это выглядит примерно так, если предположить, что два разных args ({ modelName: string, idFieldName?: K } и { [P in K]: boolean } находятся в файле объявлений .d.ts! (и если вы можете объяснить, какPropertyKey работает на вашей игровой площадке, было бы здорово!)

philolegein 07.07.2024 14:02

Хм, теперь этот вопрос выглядит совсем по-другому. Что означает «Я пытаюсь создать определение типа, которое зависит от ввода пользователя»? На первый взгляд это невозможно. Какой «пользователь» вводит данные до времени компиляции? Это человек, пишущий код TypeScript?

jcalz 09.07.2024 21:19

(см. предыдущий комментарий) Чем больше я перечитываю этот вопрос, тем больше я сбиваюсь с толку по поводу того, что вы пытаетесь сделать. В чем разница между IdField и IdFieldKey? Мне действительно нужно увидеть пример использования этой вещи.

jcalz 09.07.2024 21:27
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
12
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы обратиться к примеру MyExtension в вопросе, не беспокоясь об отношениях между modelName и idFieldName, я бы написал такое объявление:

declare function MyExtension<K extends PropertyKey = "id">(
    arg: { modelName: string, idFieldName?: K }
): {
    MyExtensionsMethod(arg: { [P in K]: boolean }): Promise<void>;
}

Здесь функция общая в K, тип idFieldName. Этот тип ограничен до PropertyKey (это всего лишь псевдоним string | number | symbol, то есть типов, которые могут быть ключами для свойств) и который по умолчанию устанавливает на id (значение по умолчанию произойдет только в том случае, если K невозможно вывести, т.е. , когда для idFieldName не передается аргумент.

Затем он возвращает объект с методом, принимающим аргумент типа {[P in K]: boolean}, который аналогичен Record<K, boolean> при использовании типа утилиты Record . Это простой отображаемый тип, который имеет свойство с ключом типа K и значением типа boolean (и это, вероятно, то, что вы имели в виду под {[K]: boolean}).

Давайте проверим это:

const UserMethods1 = MyExtension({ modelName: 'foo', idFieldName: 'bar' });
const newValue1 = await UserMethods1.MyExtensionsMethod({ bar: true });
    
const UserMethod2 = MyExtension({ modelName: 'foo' });
const newValue2 = await UserMethod2.MyExtensionsMethod({ id: true });

Выглядит неплохо. Для UserMethods1.MyExtensionMethod ожидается {bar: boolean}, тогда как для UserMethod2.MyExtensionsMethod ожидается {id: boolean}.

Если вам нужно связать modelName с idFieldName, вы, вероятно, можете сделать это вдвойне общим, чтобы у вас было

declare function MyExtension<M extends ModelNames, K extends KeysFor<M> | "id" = "id">(
    arg: { modelName: M, idFieldName?: K }
): {
    MyExtensionsMethod(arg: { [P in K]: boolean }): Promise<void>;
}

где ModelNames и KeysFor определены правильно, но я буду считать, что это выходит за рамки, поскольку эту часть должно быть достаточно легко подключить.

Детская площадка, ссылка на код

1-е: потрясающая помощь, спасибо за работу! 2-й: ваш ответ работает так, как рекламируется, но не совсем так, потому что мне нужны экспортированные фактические типы. Переписал вопрос еще 1 (и, надеюсь, последний) раз.

philolegein 09.07.2024 15:11

🙃 Ну что ж, наверное, надо посмотреть, что по сути является новым вопросом, и написать другой ответ? Новая версия вашего вопроса теперь, похоже, требует... циклической зависимости модуля? И какой «пользователь» решает, каким должен быть `${idFieldName}`? Я озадачен тем, как этот вопрос, кажется, вырвался из-под меня. Я думал, что понял, что вы делаете, и потратил время на написание ответа, который сейчас выглядит... бесполезным? Ну что ж.

jcalz 09.07.2024 21:24

Собственно, отметив это как правильное. Единственным дополнительным шагом, который мне понадобился, чтобы дойти до конца, был тип утилиты Parameters.

philolegein 15.07.2024 12:28

Другие вопросы по теме