Typescript Generic с неявным типом атрибута

#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 за то, что он рассказал мне об этом.