Файл объявления Typescript, работающий с универсальными и объединенными объектами

Я создал функцию проверки объекта конфигурации, которая в основном проверяет соответствие объекта чертежу. Очень похоже на то, как работает React Prop Types. Я буду использовать это для автоматизации развертывания приложений на разных веб-сайтах с разными файлами конфигурации, чтобы убедиться, что файл конфигурации правильно определен перед попыткой развертывания.

Работает это так:

У меня есть функция, которая принимает объект и возвращает функцию.

Пример:

const blueprint = {
stringValue: ConfigTypes.string, 
requirednumberValue: ConfigTypes.number.isRequired, 
boolOrStringValue: ConfigTypes.oneOfType([ConfigTypes.string, ConfigType.bool])
} //The syntax here is very similar to that of React Prop Types. I am essentially defining what I expect my object to look like.

const checker = ConfigChecker(blueprint)

ConfigChecker берет схему того, какие ключи мы ожидаем быть в объекте, какие типы значений для указанных ключей, и являются ли они необязательными или обязательными. ConfigChecker возвращает функцию, которая принимает в качестве аргументов 2 объекта.

Пример:

const config = {
stringValue: "Hello"
boolOrStringValue: true
}
const defaults = {
requirednumberValue: 5,
boolOrStringValue: false
}
checker(config, defaults) //checker is defined in the above example. The return of ConfigChecker(blueprint)

Аргумент config - это объект конфигурации, который мы планируем использовать для приложения, тогда как аргумент defaults - это пары значений ключа по умолчанию, которые мы можем использовать для приложения, если они не указаны в объекте конфигурации.

Внутри аргумент config и аргумент defaults глубоко объединены вместе с объектом конфигурации, перезаписывая одинаковые значения ключей в объекте значений по умолчанию.

Таким образом, результатом приведенного выше примера будет:

{
stringValue: "Hello"
boolOrStringValue: true
requirednumberValue: 5,
}

После объединения двух аргументов они проверяются на blueprint, чтобы убедиться, что конечный объект конфигурации, содержащий объединенные объекты default и config, определен правильно.

Файл объявления для этой функции выглядит так:

interface Requireable {}
interface ConfigType {
  isRequired: Requireable;
}

type CType = {
  /**
   * A string value.
   */
  string: ConfigType;
  /**
   * A boolean value.
   */
  bool: ConfigType;
  /**
   * A number value.
   */
  number: ConfigType;
  /**
   * A function.
   */
  func: ConfigType;
  /**
   * An object. Not an array.
   */
  object: ConfigType;
  /**
   * An array.
   */
  array: ConfigType;
  /**
   * Any value.
   */
  any: ConfigType;
  /**
   * An array of a specific type.
   */
  arrayOf: (type: CType[keyof CType]) => ConfigType;
  /**
   * An object containing a specific type.
   */
  objectOfType: (type: CType[keyof CType]) => ConfigType;
  /**
   * One of these values.
   * @example
   * OneOf(["Hello", "Goodbye", false])
   */
  oneOf: (enums: Array<any>) => ConfigType;
  /**
   * One of these types.
   * @example
   * OneOf([ConfigType.string, ConfigType.number])
   */
  oneOfType: (types: Array<CType[keyof CType]>) => ConfigType;
  /**
   * An object with specific keys and value types.
   */
  objectOf: (obj: { [key: string]: CType[keyof CType] }) => ConfigType;
  /**
   * An object with specific keys and value types. The objects must strictly match.
   */
  exactObjectOf: (obj: { [key: string]: CType[keyof CType] }) => ConfigType;
};

/**
 * Check functions.
 */
export const ConfigTypes: CType;

type Id<T> = { [K in keyof T]: T[K] };

type SpreadProperties<L, R, K extends keyof L & keyof R> = {
  [P in K]: L[P] | Exclude<R[P], undefined>;
};

type OptionalPropertyNames<T> = {
  [K in keyof T]: undefined extends T[K] ? K : never;
}[keyof T];

type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  Pick<L, Exclude<keyof L, keyof R>> &
    // Properties in R with types that exclude undefined
    Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>> &
    // Properties in R, with types that include undefined, that don't exist in L
    Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>> &
    // Properties in R, with types that include undefined, that exist in L
    SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

export default function <S extends { [key: string]: CType[keyof CType] }>(
  schema: S
): <C extends { [key: string]: any }, D extends { [key: string]: any }>(
  config: C,
  defaults?: D
) => Pick<Spread<C, D>, keyof S>;

По сути, этот файл декларации заявляет, что результирующее значение вызова ConfigChecker(blueprint)(config, defaults) должно быть объектом, содержащим только ключи внутри объекта blueprint с типами значений этих ключей, поступающими из объединенных объектов config и defaults. Следовательно, если бы я добавил ключ к объекту config без предварительного определения ключа в объекте blueprint, возвращаемый объект не содержал бы этот добавленный ключ; в возвращаемом объекте будут определены только ключи внутри объекта blueprint.

Хотя это хорошо работает, это просто обходной путь в отношении того, что я действительно пытаюсь сделать.

Чего я действительно хотел бы добиться от моего файла декларации, так это следующего:

Учитывая blueprint выше, машинописный текст должен сделать вывод, что возвращаемый объект до предоставления объектов config и defaults должен выглядеть следующим образом:

{
stringValue?: string, 
requirednumberValue: number, 
boolOrStringValue?: boolean | string
}

Затем, после добавления объектов config и defaults, машинописный текст должен перекрестно ссылаться на них на чертеж и возвращать что-то вроде этого:

{
stringValue: string, 
requirednumberValue: number, 
boolOrStringValue: boolean
}

Итак, во-первых, Typescript должен сделать вывод, что тип возвращаемого значения функции должен быть объектом, содержащим необязательные и обязательные пары ключ / значение. Эти пары ключ / значение могут быть нескольких типов, как в случае с: boolOrStringValue?: boolean | string, который является необязательным и может быть логическим или строковым.

Наконец, Typescript должен читать значения в объединенных объектах config и defaults и заменять предполагаемые типы на известные типы.

Я знаю, что первая работа работает нормально, однако, помимо того, что это классная функция, она также позволит мне сосредоточиться на Typescript, поскольку я только начал. Я люблю бросаться в ловушку, делая такие вещи. Можно ли что-то подобное сделать или я не в себе?

Заранее спасибо.


person Daniel Mastrorillo    schedule 13.12.2020    source источник


Ответы (1)


Мне удалось найти собственное решение. Это новый файл декларации:

interface OConfigType<T> {
  isRequired: RConfigType<T>;
}

interface RConfigType<_T> {
  (): void;
}

type GetType<T> = T extends OConfigType<infer T> | RConfigType<infer T>
  ? T
  : never;

type CType = {
  /**
   * A string value.
   */
  string: OConfigType<string>;
  /**
   * A boolean value.
   */
  bool: OConfigType<boolean>;
  /**
   * A number value.
   */
  number: OConfigType<number>;
  /**
   * A function.
   */
  func: OConfigType<Function>;
  /**
   * An object. Not an array.
   */
  object: OConfigType<Object>;
  /**
   * An array.
   */
  array: OConfigType<Array<any>>;
  /**
   * Any value.
   */
  any: OConfigType<any>;
  /**
   * An array of a specific type.
   */
  arrayOf: <S>(type: S) => OConfigType<Array<GetType<S>>>;
  /**
   * An object containing a specific type.
   */
  objectOfType: <S>(
    type: S
  ) => OConfigType<Record<string | number, GetType<S>>>;
  /**
   * One of these values.
   * @example
   * OneOf(["Hello", "Goodbye", false])
   */
  oneOf: <T>(
    enums: [...T]
  ) => T extends Array<infer U> ? OConfigType<U> : never;
  /**
   * One of these types.
   * @example
   * OneOf([ConfigType.string, ConfigType.number])
   */
  oneOfType: <T>(types: [...T]) => T extends Array<infer U> ? U : never;
  /**
   * An object with specific keys and value types.
   */
  objectOf: <S extends { [key: string | number]: OConfigType | RConfigType }>(
    obj: S
  ) => OConfigType<ReturnSchema<S>>;
  /**
   * An object with specific keys and value types. The objects must strictly match.
   */
  exactObjectOf: <
    S extends { [key: string | number]: OConfigType | RConfigType }
  >(
    obj: S
  ) => OConfigType<ReturnSchema<S>>;
};

/**
 * Check functions.
 */
export const ConfigTypes: CType;

type Id<T> = { [K in keyof T]: T[K] };

type SpreadProperties<L, R, K extends keyof L & keyof R> = {
  [P in K]: L[P] | Exclude<R[P], undefined>;
};

type OptionalPropertyNames<T> = {
  [K in keyof T]: undefined extends T[K] ? K : never;
}[keyof T];

type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  Pick<L, Exclude<keyof L, keyof R>> &
    // Properties in R with types that exclude undefined
    Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>> &
    // Properties in R, with types that include undefined, that don't exist in L
    Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>> &
    // Properties in R, with types that include undefined, that exist in L
    SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

// export default function <S extends { [key: string]: CType[keyof CType] }>(
//   schema: S
// ): <C extends { [key: string]: any }, D extends { [key: string]: any }>(
//   config: C,
//   defaults?: D
// ) => Pick<Spread<C, D>, keyof S>;

//type ReturnType<S> =

type Filter<Base, Condition> = {
  [Key in keyof Base]: Base[Key] extends Condition ? Key : never;
}[keyof Base];

type FilterReturnType<S> = {
  [Key in keyof S]: S[Key] extends OConfigType | RConfigType ? Key : never;
}[keyof S];

type GetReturnType<S> = {
  [Key in keyof S]: S[Key] extends OConfigType<infer T> | RConfigType<infer T>
    ? T
    : never;
};

type ReturnType<S> = GetReturnType<Pick<S, FilterReturnType<S>>>;

type ReturnSchema<S> = ReturnType<Pick<S, Filter<S, RConfigType<any>>>> &
  ReturnType<Partial<Pick<S, Filter<S, OConfigType<any>>>>>;

type ReturnConfig<S, C> = {
  [K in keyof S]: C[K];
};

interface ReturnFunction<S> {
  <C extends S, D extends S>(config: C, defaults?: D = {}): ReturnConfig<
    S,
    Spread<C, D>
  >;
}

export default function <
  S extends {
    [key: string | number]: OConfigType | RConfigType;
  }
>(schema: S): ReturnFunction<ReturnSchema<S>>;

Это делает большую часть того, что я пытался достичь, кроме чтения значений config и defaults для обновления конечного объекта. Но, по крайней мере, теперь вывод функции основан на добавленном вами объекте blueprint.

Теперь ожидаемый ввод для объектов config и defaults правильно определен:

Скриншот VSCode

И выходной объект правильно определен.

Скриншот VSCode

person Daniel Mastrorillo    schedule 14.12.2020