Рекурсивный вызов метода Typescript для свойства и метода Omit - PullRequest
1 голос
/ 25 сентября 2019

У меня есть такая спецификация

type SpecificType<U, T extends keyof U> = {
    Name: T,
    Value: U[T]
}

class Builder<TItems> {
    private items: SpecificType<TItems, any>[] = [];

    and<TType extends keyof TItems>(type: TType, value: TItems[TType]): Builder<Omit<TItems, TType>> {
        return this.append(this.create(type, value));
    }

    append<TType extends keyof TItems>(item: SpecificType<TItems, TType>): Builder<Omit<TItems, TType>> {
        this.items.push(item);
        return this as Builder<Omit<TItems, TType>>;
    }

    build(): SpecificType<any, any>[] {
        return this.items;
    }

    private create<TType extends keyof TItems>(type: TType, value: TItems[TType]): SpecificType<TItems, TType> {
        return {
            Name: type,
            Value: value
        }
    }
}

Строитель строит элементы на основе универсального типа, который в основном сопоставляет имя и тип следующим образом:

type Types = {
    "Type1": undefined,
    "Type2": { MaxAge: number }
}

И вот какЯ использую строитель

const itemBuilder = new Builder<Types>();
itemBuilder
    .and("Type1", undefined)
    .and("Type2", { MaxAge: 3 });

У меня есть 2 вопроса

  1. Я опускаю конкретное сопоставление типов при каждом вызове and или append, потому что эти методы могут вызываться толькоодин раз за конкретный key в TItems (или в этом случае Types).Можно ли также удалить and и append, когда все ключи TItems были использованы?В настоящее время, если я позвоню другому and на itemBuilder, автозаполнение предложит это: and(type: never, value: never).Было бы намного лучше, если бы не было and и append в автозаполнении для itemBuilder после этих первых двух вызовов.
  2. Если вы посмотрите на первый and вызов на itemBuilder, так как Type1 отображение undefined, я должен передать undefined в качестве значения.Можно ли как-то передать просто имя методу and, если у него нет сопоставления - потому что в результате SpecificType просто { Name: string } и мне не нужно value (или я мог бы объявить сопоставление как-то иначе, не используяundefined может ??)

1 Ответ

2 голосов
/ 25 сентября 2019

Прежде всего, я перехожу к соглашению об именах, где параметры типа представляют собой одну или две заглавные буквы, где 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() отсутствуют.

Это дает вам то, что вы хотели, я думаю.


Хорошо, надеюсь, это поможет.Удачи!

Ссылка на код

...