如何从描述它的模式创建类型? [英] How can I create a type from a schema that describes it?

查看:18
本文介绍了如何从描述它的模式创建类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

For my own learning I'm trying to create something like an ODM, but I'm having a lot of trouble figuring out how to create type definitions from a schema describing a type of document. For some reason type checking for instance isn't working and suggestions don't work (instance seems to act as if it's any so the editor doesn't know what properties it has). Any help would be greatly appreciated, thank you.

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 类似于:

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

推荐答案

您有两个选择来处理它.

You have two options to handle it.

第一个


/**
 * Please refer to this link for explanation
 * https://stackoverflow.com/a/50375286
 */
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>

游乐场

第二

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

游乐场

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

Personaly, I think that it is better to use required property instead of optional and accordingly reverse the boolean flags.

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

I mean to use {required:true} instead of {optional: false}.

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

It is more readable, but again, it is only my opinion.

请查看这个问题.这个案例和你的很相似

Please take a look at this question. This case is VERY similar to yours

这篇关于如何从描述它的模式创建类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆