#typescript #generics #types
#typescript #дженерики #типы
Вопрос:
Контекст
У меня возникли проблемы с выяснением, могу ли я использовать подразумеваемые значения для общих аргументов в типах, так же, как это возможно в функциях.
Предположим, у меня есть следующий интерфейс, определяющий структуру некоторых таблиц моей базы данных:
interface Tables {
'table-users': { userId: string, username: string },
'table-posts': { postId: string, userId: string, title: string },
}
Я могу написать упрощенную функцию, которая позволяет мне записывать в эти таблицы, используя:
const write = <T extends keyof Tables>(table: T, item: Tables[T]): boolean => {
// ... do something
return true;
}
Это позволяет мне делать
write('table-users', { userId: '123', username: 'John Doe' }); // ✅
write('table-posts', { postId: 'abc', userId: '123', title: 'First Post' }); // ✅
Но не
write('table-users', { postId: 'abc' }); // ❌
write('table-posts', { userId: 'abc' }); // ❌
Пока все хорошо.
Проблема
Однако теперь я хотел бы иметь возможность создавать тип (аналогичный функции), который принимает свойство, которое должно быть именем таблицы, а другое свойство этого типа должно иметь правильный формат элемента.
Я подумал, что что-то в этом роде должно сработать:
type Item<T extends keyof Tables> = {
table: T,
item: Tables[T],
}
и автоматически разрешать:
const userItem: Item = {
table: 'table-users',
item: {
userId: '1',
username: 'John Doe',
}
};
const postItem: Item = {
table: 'table-posts',
item: {
postId: '123',
userId: '1',
username: 'John Doe',
}
};
или просто используйте Item
в качестве параметра функции typehint , но это не так, потому что это всегда требует явного определения имени таблицы:
const userItem: Item<'table-users'> = {
table: 'table-users',
item: {
userId: '1',
username: 'John Doe',
}
};
const postItem: Item<'table-posts'> = {
table: 'table-posts',
item: {
postId: '123',
userId: '1',
title: 'First Post',
}
};
Любая подсказка, возможно ли то, что я ищу, в TypeScript или нет? Я думаю, что так и должно быть, потому что это могло бы быть, и я, вероятно, просто делаю что-то глупое, но, похоже, я не могу это исправить.
Редактировать
Напротив, это действительно так:
type Item = {
[T in keyof Tables]?: {
item: Tables[T]
}
}
но я надеюсь сделать то же самое, но использовать T в качестве значения, а не ключа.
Ответ №1:
После долгих исследований и тестирования, я думаю, я придумал кое-что, что может вам помочь.
Если я правильно понимаю проблему, вы хотите Item
, чтобы свойство вашего типа table
было ограничено значениями, которые соответствуют ключам вашего ранее определенного Tables
интерфейса.
Для этого вы можете просто написать свой тип элемента следующим образом:
type Item = {
table: keyof Tables,
item: Tables[keyof Tables]
}
Используя это, ваш Item
объект будет иметь table
свойство, ограниченное ключами вашего Tables
интерфейса. Аналогично, ваше item
свойство будет ограничено значениями вашего Tables
интерфейса.
Это позволяет обойти необходимость явного вызова имени таблицы с помощью общего <>
синтаксиса.
Однако это НЕ приведет к тому, что значение вашей таблицы будет соответствовать значениям элементов.
// This is allowed, but not expected as table-users doesn't allow for postId
const postItem: Item = {
table: 'table-users',
item: {
postId: '123',
userId: '1',
username: 'John Doe',
}
};
Если вы хотите, чтобы в вашем табличном значении принудительно присваивалось допустимое значение Item.item
, вам, возможно, придется передать значение явно.
Кроме того, вы можете изменить логику восходящего потока, чтобы Item
не ссылаться на собственную таблицу. Это также упростило бы ввод текста.
type Item<T extends keyof Tables> = Tables[T]
Я думаю, что проблема, лежащая в основе этого вопроса, заключается в том, что Item.item
нужно было бы ссылаться на значение Item.table
, и нет способа сделать это без явного предоставления его через дженерики. У нас нет способа самостоятельно ссылаться на значение, которое присваивает пользователь Item.table
, чтобы обеспечить ввод для Item.item
соответствия этому табличному значению.
Комментарии:
1. Привет, Кевин, я ценю, что ты разбираешься в этом. Проблема, которую вы объясняете в своем примере смешивания имени таблицы и структуры элемента, действительно является тем, чего я не хочу. Я также начинаю верить, что то, чего я хочу, невозможно. Однако мне по-прежнему кажется странным, что это работает в сигнатуре функции (как показано в моем примере), но опять же, с объектом вам придется учитывать мутации после создания. Спасибо! Я оставлю это дело в покое.
Ответ №2:
Вы можете использовать условные типы следующим образом:
type ItemGeneric<T> = T extends keyof Tables ? {
table: T;
item: Tables[T];
} : never;
type Item = ItemGeneric<keyof Tables>;
Это неявно установит тип item
на основе заданного вами строкового значения table
. Вот небольшая демонстрация этого в действии.
Полная благодарность nmain на github за то, что он рассказал мне об этом.