Прежде всего, я перехожу к соглашению об именах, где параметры типа представляют собой одну или две заглавные буквы, где T
и U
означают общие типы, а K
и P
для ключевых типов.Это более стандартно для TypeScript, по любой причине.
Вот как я это сделаю.Для вопроса 1 я создам псевдоним типа NextBuilder
, который выглядит следующим образом:
type NextBuilder<T, K extends keyof T> = keyof T extends K
? Omit<Builder<{}>, "and" | "append">
: Builder<Omit<T, K>>;
Это условный тип , который проверяет, равно ли keyof T
(илиуже) K
.Если так, это означает, что Omit<T, K>
будет {}
, и есть случай, с которым мы пытаемся разобраться.В таких случаях мы вернем Omit<Builder<{}>, "and" | "append">
, который скрывает методы and()
и append()
, как вы хотите.В противном случае Omit<T, K>
будет по-прежнему иметь некоторые известные свойства, и мы вернем Builder<Omit<T, K>>
, как и ваш исходный код.Затем мы изменяем add()
и append()
, чтобы они возвращали NextBuilder<T, K>
(и используем некоторые разумные утверждения типов, чтобы успокоить компилятор, который обычно не может проверять присваиваемость неразрешенным условным типам), и все готово.Например, вот новый append()
:
append<K extends keyof T>(item: SpecificType<T, K>): NextBuilder<T, K> {
this.items.push(item);
return this as any;
}
Для вопроса 2 я бы изменил сигнатуру and()
, чтобы второй параметр был необязательным в случаях, когда undefined
является возможным значениемсобственности.(Так что это также должно позволить вам отключить value
, если у вас есть type Types = {Type3?: string}
. Изменение немного странное, и мне пришлось перепрыгнуть через несколько обручей, чтобы заставить его хорошо себя вести на месте вызова. Вот новый and()
:
and<K extends keyof T>(
type: K,
...[value]: undefined extends T[K]
? Parameters<(value?: T[K]) => void>
: Parameters<(value: T[K]) => void>
): NextBuilder<T, K> {
return this.append(this.create(type, value!)) as any;
}
Хорошо, так что первый параметр просто type: K
. Давайте рассмотрим второй. Он начинается с ...[value]
, что означает, что мы используем rest параметр массив и сразу деструктурирование его в value
. После этого следует аннотация типа параметра rest, :
. Тип массива rest параметра является условным типом, условие которого undefined extends T[K] ?
.Если undefined extends T[K]
равно true, тогда тип возвращаемого значения - Parameters<(value?: T[K]) => void>
, который использует служебный тип Parameters
для получения необязательного одн кортежа типа [T[K]?]
. В противном случаетип возвращаемого значения Parameters<(value: T[K]) => void>
, который имеет тип [T[K]]
. Честно говоря, это намного сложнее, чем просто undefined extends T[K] ? [T[K]?] : [T[K]]
, но у него есть преимущество перед первым в том, что он дает аргументу имя value
в IntelliSenseа не просто что-то выдуманноекак arg0
(есть немного документации , в которой упоминается, если вы выводите параметры из типа функции в кортеж, а затем помещаете его обратно в параметры функции, исходные имена параметров сохраняются).
Обратите внимание, что в реализации мне пришлось использовать оператор ненулевого подтверждения (!
) для value
, так как компилятор не может проверить, что T[K] | undefined
являетсяприемлемый второй параметр для this.create()
.Ненулевое утверждение - это короткий способ сказать, что value
на самом деле T[K]
(что мы знаем, так как это только undefined
, если undefined extends T[K]
верно).
Вот так!? Так вот и все.
Давайте отступим и посмотрим, как это работает:
const i1 = itemBuilder.and("Type1"); // IntelliSense shows:
// (method) Builder<Types>.and<"Type1">(
// type: "Type1", value?: undefined
// ): Builder<Pick<Types, "Type2">>
const i2 = i1.and("Type2", { MaxAge: 3 }); // IntelliSense shows:
// (method) Builder<Pick<Types, "Type2">>.and<"Type2">(
// type: "Type2", value: { MaxAge: number; }
// ): Pick<Builder<{}>, "build">
const i3 = i2.build(); // IntelliSense shows:
// (method) build(): SpecificType<any, any>[]
Это выглядит хорошо для меня.Когда вы впервые используете and()
, вам предлагается ввести либо "Type1"
, либо "Type2"
в качестве первого параметра.Как только вы выберете "Type1"
, IntelliSense покажет, что value
является необязательным вторым параметром, и вы можете его пропустить.Следующая and()
позволяет вам выбрать "Type2"
, а value
является обязательным вторым параметром.Наконец, после этого вам разрешено звонить только build()
, поскольку add()
и append()
отсутствуют.
Это дает вам то, что вы хотели, я думаю.
Хорошо, надеюсь, это поможет.Удачи!
Ссылка на код