Мне нужно выбрать только два свойства, имена которых еще не определены из типа, и создать из них новый тип с одним из этих свойств обязательным и еще одним необязательным.
Я знаю, что можно выбрать одно свойство с помощью
<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.
Соответствует ли этот подход вашим потребностям? Если это так, я мог бы написать ответ, объясняющий это; если нет, то что мне не хватает?
Да @jcalz, это соответствует моим потребностям. Пожалуйста, дайте ответ, объясняющий это!
Я сделаю это, когда у меня будет шанс; может быть через несколько часов.
Во-первых, давайте создадим вспомогательный тип с именем 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!
Успех!
Ссылка на код для игровой площадки
Это прекрасное объяснение и блестящее исполнение. Спасибо.
Предоставьте минимальный воспроизводимый пример, который четко демонстрирует проблему, с которой вы столкнулись. В идеале кто-то мог бы вставить код в автономную IDE, такую как Площадка TypeScript (ссылка здесь!), и сразу же приступить к решению проблемы, не создавая ее заново. Таким образом, не должно быть псевдокода, опечаток, несвязанных ошибок или необъявленных типов или значений.