Выберите только два свойства из типа в Typescript

Мне нужно выбрать только два свойства, имена которых еще не определены из типа, и создать из них новый тип с одним из этих свойств обязательным и еще одним необязательным.

Я знаю, что можно выбрать одно свойство с помощью

<T extends Record<string,any>> {
    [K in keyof T]: (Record<K, T[K]> & 
    Partial<Record<Exclude<keyof T, K>, never>>) extends infer U ? { [P in keyof U]: U[P] } : never
}[keyof T]

Но не понял, как (и если) можно выбрать два свойства с помощью этого метода.

Следует пример того, как я хотел бы его использовать

class Article {
    name: string
    id: number
    content?: string
}

const article: TwoKeys<Article> = { id: 23 } // no error
const article: TwoKeys<Article> = { name: "my article", id: 122 } // no error
const article: TwoKeys<Article> = { name: "my article" , id: 23, content: "my content" } // error! we passed more than two props.

Предоставьте минимальный воспроизводимый пример, который четко демонстрирует проблему, с которой вы столкнулись. В идеале кто-то мог бы вставить код в автономную IDE, такую ​​как Площадка TypeScript (ссылка здесь!), и сразу же приступить к решению проблемы, не создавая ее заново. Таким образом, не должно быть псевдокода, опечаток, несвязанных ошибок или необъявленных типов или значений.

jcalz 15.05.2022 01:49

Соответствует ли этот подход вашим потребностям? Если это так, я мог бы написать ответ, объясняющий это; если нет, то что мне не хватает?

jcalz 15.05.2022 02:01

Да @jcalz, это соответствует моим потребностям. Пожалуйста, дайте ответ, объясняющий это!

soffyo 15.05.2022 08:45

Я сделаю это, когда у меня будет шанс; может быть через несколько часов.

jcalz 15.05.2022 15:25
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
4
47
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Во-первых, давайте создадим вспомогательный тип с именем PickOnly<T, K>, где вы берете объектно-подобный тип T и ключевой тип K (или союз таких ключей) и создаете новый объектно-подобный тип, в котором свойства T с ключами в K известны присутствовать (так же, как тип утилиты Pick<T, K>), и где заведомо отсутствуют ключи нет в T (что не требуется в Pick<T, K>):

type PickOnly<T, K extends keyof T> =
    Pick<T, K> & { [P in Exclude<keyof T, K>]?: never };

Реализация пересекаетсяPick<T, K> с типом, запрещающим ключи в T, кроме тех, что в K. Тип {[P in Exclude<keyof T, K>]?: never} использует тип утилиты Exclude<T, U> для получения не-K ключей T и говорит, что все они должны быть необязательные свойства, тип значения которых невозможный тип never. Необязательное свойство может отсутствовать (или undefined в зависимости от параметров компилятора), но свойство never не может присутствовать... это означает, что эти свойства всегда должны отсутствовать (или undefined).

Пример:

let x: PickOnly<{a: string, b: number, c: boolean}, "a" | "c">;
x = {a: "", c: true} // okay
x = {a: "", b: 123, c: true} // error!
// -------> ~
//Type 'number' is not assignable to type 'never'.
x = {a: ""}; // error! Property 'c' is missing

Значение типа X должно быть {a: number, c: boolean} и, кроме того, вообще не может содержать свойство b.


Таким образом, желаемое вами AtMostTwoKeys<T> предположительно представляет собой объединение PickOnly<T, K> для каждого K, состоящего из всех возможных наборов ключей в T, состоящих не более чем из двух элементов. Для Article это выглядит как

| PickOnly<Article, never> // no keys
| PickOnly<Article, "name"> // only name
| PickOnly<Article, "id"> // only id
| PickOnly<Article, "content"> // only content
| PickOnly<Article, "name" | "id"> // name and id
| PickOnly<Article, "name" | "content"> // name and content
| PickOnly<Article, "id" | "content"> // id and content

Итак, давайте строить AtMostTwoKeys<T>. Часть без ключей проста:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |    
)'

Теперь об одном ключе... самый простой способ сделать это - через тип объекта дистрибутива формы, придуманной в Майкрософт/TypeScript#47109. Тип формы {[K in KK]: F<K>}[KK], где вы сразу индексировать в a отображаемый тип производит объединение F<K> для всех K в KK союзе.

Итак, для одного ключа это выглядит так:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]: PickOnly<T, K> }[keyof T]
);

О, но in keyof T делает сопоставленный тип гомоморфный, который, возможно, будет вводить нежелательные undefined значения в вывод для необязательных входных свойств, я заранее буду используйте модификатор сопоставленного типа -?, чтобы удалить модификатор необязательности из сопоставления:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> }[keyof T]
);

С двумя ключами все немного сложнее. Здесь мы хотим сделать два слоев дистрибутивных объектов. Первый выполняет итерацию по каждому ключу K в keyof T, а второй должен ввести новый параметр типа (скажем, L), чтобы сделать то же самое. Тогда K | L будет любой возможной парой ключей из keyof T, а также каждым отдельным ключом (когда K и L совпадают). Это дважды считает разные пары, но это ничему не повредит:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> |
        { [L in keyof T]-?:
            PickOnly<T, K | L> }[keyof T]
    }[keyof T]
) 

Это в основном так, но результирующий тип будет выражен через PickOnly:

type AMTKA = AtMostTwoKeys<Article>;
/* type AMTKA = PickOnly<Article, never> | PickOnly<Article, "name"> | 
  PickOnly<Article, "name" | "id"> | PickOnly<Article, "name" | "content"> | 
  PickOnly<Article, "id"> | PickOnly<Article, "id" | "content"> | \
  PickOnly<Article, "content"> */

Может быть, это нормально. Но обычно мне нравится вводить небольшой помощник для расширить такие типы в их фактические свойства:

type AtMostTwoKeys<T> = (
    PickOnly<T, never> |
    { [K in keyof T]-?: PickOnly<T, K> |
        { [L in keyof T]-?:
            PickOnly<T, K | L> }[keyof T]
    }[keyof T]
) extends infer O ? { [P in keyof O]: O[P] } : never

Давайте попробуем еще раз:

type AMTKA = AtMostTwoKeys<Article>;
/* type AMTKA = 
| {  name?: never;  id?: never;  content?: never; } // no keys
| {  name: string;  id?: never;  content?: never; } // only name
| {  name: string;  id: number;  content?: never; } // name and id
| {  name: string;  content?: string;  id?: never; } // name and content
| {  id: number;  name?: never;  content?: never; }  // only id
| {  id: number;  content?: string;  name?: never; } // id and content
| {  content?: string;  name?: never;  id?: never; } // only content
*/

Выглядит неплохо!


И чтобы быть уверенным, давайте проверим ваши примеры использования:

let article: AtMostTwoKeys<Article>;
article = { id: 23 } // okay
article = { name: "my article", id: 122 } // okay
article = { name: "my article", id: 23, content: "my content" } // error!

Успех!

Ссылка на код для игровой площадки

Это прекрасное объяснение и блестящее исполнение. Спасибо.

soffyo 15.05.2022 23:47

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