使用泛型和合并对象的 Typescript 声明文件

Posted

技术标签:

【中文标题】使用泛型和合并对象的 Typescript 声明文件【英文标题】:Typescript Declaration File working with generics and merged objects 【发布时间】:2021-03-24 03:11:03 【问题描述】:

我创建了一个配置对象检查器功能,它基本上检查对象是否与蓝图匹配。非常类似于 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,

在 2 个参数合并后,它们将针对 blueprint 进行测试,以确保正确定义包含合并的 defaultconfig 对象的最终配置对象。

此函数的声明文件如下所示:

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对象内的键,这些键的值类型来自合并的configdefaults 对象。因此,如果我在没有先在blueprint 对象中定义键的情况下向config 对象添加键,则返回的对象将不包含该添加的键;只有blueprint 对象中的键会在返回的对象中定义。

虽然这很好用,但它只是我真正想要做的一种解决方法。

我真正想从我的声明文件中实现的是:

鉴于上面的blueprint,打字稿应该推断出返回的对象,在提供configdefaults对象之前,应该是这样的:


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

然后,在添加 configdefaults 对象后,typescript 应该将它们交叉引用到蓝图并返回如下内容:


stringValue: string, 
requirednumberValue: number, 
boolOrStringValue: boolean

首先,Typescript 应该推断函数的返回类型应该是一个包含可选和必需键/值对的对象。这些键/值对可以是多种类型,例如:boolOrStringValue?: boolean | string,它既是可选的,也可以是布尔值或字符串。

最后,Typescript 应该读取合并的 configdefaults 对象中的值,并将假定的类型替换为已知类型。

我知道第一个解决方法可以正常工作,但是,除了这是一个很酷的功能之外,它还可以让我在刚刚开始的时候就开始使用 Typescript。我喜欢做这样的事情让自己陷入困境。可以做这样的事情吗?还是我在我头上?

提前致谢。

【问题讨论】:

【参考方案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>>;

除了读取configdefaults 的值以更新最终对象之外,这完成了我试图实现的大部分工作。但至少现在函数的输出是基于你输入的blueprint 对象。

现在configdefaults 对象的预期输入已正确定义:

并且输出对象被正确定义。

【讨论】:

以上是关于使用泛型和合并对象的 Typescript 声明文件的主要内容,如果未能解决你的问题,请参考以下文章

扩展类型的泛型和 Typescript 中的普通类型有啥区别?

打字稿、泛型和重载

TypeScript 基础学习之泛型和 extends 关键字

泛型和Object的区别?

在 Delphi XE2 中使用泛型和前向声明时的编译器错误

七数组和集合(一维数组和二维数组的声明以及使用,ArrayList类,HashTable,List,Directory字典等常用集合(泛型和非泛型))