如何从描述它的模式创建类型?

How can I create a type from a schema that describes it?

为了我自己的学习,我正在尝试创建类似 ODM 的东西,但我在弄清楚如何从描述一种文档类型的模式创建类型定义时遇到了很多麻烦。出于某种原因,instance 的类型检查不起作用并且建议不起作用(instance 似乎表现得好像它是 any 所以编辑器不知道它有什么属性) .任何帮助将不胜感激,谢谢。

type PropertyType = 'string' | 'number';

interface RequiredProperty<T> {
    default: T;
    optional?: false;
}

interface OptionalProperty<T> {
    default?: T;
    optional: true;
}

type BaseProperty<TKind extends PropertyType, TType> = (
    | RequiredProperty<TType>
    | OptionalProperty<TType>
) & {
    kind: TKind;
};

type StringType = BaseProperty<'string', string>;
type NumberType = BaseProperty<'number', number>;

type ModelProperty = StringType | NumberType;

interface Model {
    [x: string]: ModelProperty;
}

type ModelInstance<T extends Model> = {
    [K in keyof T]: T[K]['kind'] extends 'string' ? string : number;
};

const model: Model = {
    str: {
        kind: 'string',
        default: 'abc'
    },
    num: {
        kind: 'number',
        optional: true
    }
};

const instance: ModelInstance<typeof model> = {
    // Type 'string' is not assignable to type 'number'.
    str: 'test',
    num: 123
}

我希望 ModelInstance<typeof model> 是这样的:

interface Expected {
    str: string;
    num?: number;
}

你有两种选择来处理它。

第一个


/**
 * Please refer to this link for explanation
 * 
 */
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type PropertyType = 'string' | 'number';


interface RequiredProperty<T> {
  default: T;
  optional?: false;
}

interface OptionalProperty<T> {
  default?: T;
  optional: true;
}

type BaseProperty<Kind extends PropertyType, Type> = (
  | RequiredProperty<Type>
  | OptionalProperty<Type>
) & {
  kind: Kind;
};

type StringType = BaseProperty<'string', string>;
type NumberType = BaseProperty<'number', number>;

type ModelProperty = StringType | NumberType;

interface Model {
  [x: string]: ModelProperty;
}

type Values<T> = T[keyof T]

/**
 * Translates string type name to actual type
 * Logic is pretty straitforward
 * - if ['kind'] is 'string' -> string
 * - if ['kind'] is 'number' -> number
 */
type TranslateType<T extends { kind: PropertyType }> =
  T['kind'] extends 'string'
  ? string
  : T['kind'] extends 'number'
  ? number
  : never;


type GenerateData<T extends Model> =
  /**
   * Iterate throus model data structure
   */
  {
    /**
     * If ['optional'] exists and it is true
     * Clone same data structure {kind:string, default:string}
     * into nested property, make it partial and translate 'string' to string
     */
    [K in keyof T]: T[K] extends { optional: true } ? {
      -readonly [P in K]?: TranslateType<T[K]>
    } : {
      /**
       * Do same as above but without making data optional
       */
      -readonly [P in K]: TranslateType<T[K]>
    }
  };

/**
 * UnionToIntersection -> converts union to UnionToIntersection
 * Values -> obtain all nested properties as a union
 */
type ModelInstance<T extends Model> =
  UnionToIntersection<Values<GenerateData<T>>>

const model = {
  str: {
    kind: 'string',
    default: 'abc'
  },
  num: {
    kind: 'number',
    optional: true
  }
} as const;

type Result = ModelInstance<typeof model>

Playground

第二

interface OptionRequired {
  type: 'string' | 'number'
  optional: boolean
}
interface OptionPartial {
  type: 'string' | 'number'
}

type Option = OptionPartial | OptionRequired

/**
 * Translates string type name to actual type
 * Logic is pretty straitforward
 */
type TranslateType<T extends Option> =
  T['type'] extends 'string'
  ? string
  : T['type'] extends 'number'
  ? number
  : never;

/**
 * Check if optional exists
 * if false - apply never, because union of T|never produces t
 * if true - apply undefined
 */
type ModifierType<T extends Option> =
  T extends { optional: true }
  ? undefined
  : never

/**
 * Apply TranslateType 'string' -> string
 * Apply ModifierType {optional:true} -> undefined
 */
type TypeMapping<T extends Option> = TranslateType<T> | ModifierType<T>

/**
 * Apply all conditions to each option
 */
type Mapping<T> = T extends Record<string, Option> ? {
  -readonly [Prop in keyof T]: TypeMapping<T[Prop]>
} : never

type Data<Options> = Mapping<Options>

const model = {
  a: {
    type: 'string',
    optional: true,
  },
  b: {
    type: 'number',
    optional: false
  },
  c: {
    type: 'string',
  },
} as const

type Result = Data<typeof model>

declare var x:Result

type a = typeof x.a // string | undefined
type b = typeof x.b // number
type c = typeof x.c // string

Playground

我个人认为最好使用 required 属性 而不是 optional 并相应地反转布尔标志。

我的意思是使用 {required:true} 而不是 {optional: false}

它更具可读性,但同样,这只是我的意见。

请看这个。这个案例和你的非常相似