TypeScript 在类型中使用编译时常量,

chy5wohz  于 2个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(41)

在库的核心中,打字是非常有用的,如果我们能轻松地使用它进行验证,它可能会更有用。
有一些库试图在这方面改进编程体验,但由于语言本身的限制而失败。
我离搜索主题越近,就越是使用 zod 库,然而我仍然需要为类型和内容分别提供信息。
生成数据的部分可以被认为是 Non-goal

  1. 在程序中添加或依赖运行时类型信息,或者根据类型系统的结果发出不同的代码。相反,鼓励不要求运行时元数据的编程模式。
    从编译时间常量定义泛型类型更接近于消除似乎不必要的障碍,因为常量和类型接受相同的输入。

搜索词

指的是一个值,但用作类型

建议

我的建议是将编译时常量用作类型或类型的值。
目前可以以有趣的方式操作类型。

type JSONSchema = {type: string, items?: JSONSchema, properties?: {[key: string]: JSONSchema}}

type TypeFromSchema<T extends JSONSchema> = 
    T['type'] extends 'string' ? string: 
    T['type'] extends 'boolean' ? boolean:
    T['type'] extends 'number'|'integer' ? number:
    T['type'] extends 'array' ? 
        // ignore heterogeneous arrays
        (T['items'] extends JSONSchema ? TypeFromSchema<T['items']>: never):
    T['type'] extends 'object' ? {
        // neglect the required fields
        [k in keyof T['properties']]?: TypeFromSchema<Extract<T['properties'][k], JSONSchema>> 
    } : never;

type stringSchema = {"type": "string"};
type booleanSchema = {"type": "boolean"};
type numberSchema = {"type": "number"};

type objectSchema = {
    "type": "object",
    "properties": {
        "boolean": {"type": "boolean"},
        "integer": {"type": "integer"},
        "number": {"type": "number", "maxLength": 20};
        "string": {"type": "stirng", "required": true};
    }
}

let validString: TypeFromSchema<stringSchema>;  // string

let validNumber: TypeFromSchema<numberSchema>; // number

let validBoolean: TypeFromSchema<booleanSchema>; // boolean
// {boolean?: boolean, integer?: number, number?: number, string?: string}
let obj: TypeFromSchema<objectSchema> = {};

我想让 booleanSchema 作为 const 而不是 type 。或者能够提取要在运行时使用的类型的信息。

用例

这将使更有趣的数据验证成为可能,例如上面提到的 Zod 这样的库就可以仅使用类型声明来工作。

示例

我在这里给出一些可以被视为替代方案的例子

编译时常量到类型

这样我可以声明 Model 类型的变量,启用 IDE 辅助和编译时类型检查,在运行时我可以使用 model 值进行动态数据验证。

declare function Validate<T, U extends SchemaFromType<T>>(data, schema: U): T;
const X = Validate<Model>(fetchData(), model);

对象模型是可变的,所以如果我修改它,它就会变得不一致,这将表现为深拷贝,它只在执行该行时才考虑值。

类型到编译时常量(也许不是目标)

以下候选项将提供提取类型信息的机制

type Coord = {latitude: number, longitude: number, height?: number};
const CoordProperties = KeysOf<Coord>;
const CoordOptional = OptionalKeysOf<Coord>;

或者更通用的东西,如

const ScalarSchema<T extends  string | boolean | number> {
  [k keyof T]:  
    T[k] extends string ? {type: 'string'}:
    T[k] extends boolean ? {type: 'boolean'} ? 
    T[k] extends number ? {type: 'number'} : never;
}

这些构造可以在许多代码中多次使用,以避免增加太多代码的大小。

融合赋值和类型声明(给关键字 type 带来新用途)

在以下候选项中,有一个赋值和精确类型的定义合并在一个指令中。例如 TypeOf(rightAngle) = {angle: number}rightAngle 的一般化,另一方面 RightAngle = {angle: 90} 将是一个精确类型。同样的情况也适用于从 JSON 或其他模块导入数据。

const rightAngle: type RightAngleType = {angle: 90};
import modelSchema: type ModelSchemaType from 'complex-data-schema.json';

融合类型和模式定义(也许不是目标)

在以下候选项中,我们将定义类型 Color 和一个 schema,其中指定类型的写入。这可以用于任何广泛的模式,并在编译期间转换,可能需要 tsconfig.json 中指定的脚本(如果需要)。

type Color: schema colorSchema = 'red' | 'green' | 'blue';

然后 colorSchema 将被设置为

{
  "$schema": "https://json-schema.org/draft/2019-09/schema",
  "type": "string",
  "enum": ["read", "green", "blue"]
}

检查表

我的建议符合以下准则:

  • 这不会对现有的 TypeScript/JavaScript 代码造成破坏性的变化
  • 这不会改变现有 JavaScript 代码的运行时行为
  • 这可以在不根据表达式的类型发射不同的 JS 的情况下实现
  • 这不是一个运行时特性(例如库功能、非 ECMAScript 语法与 JavaScript 输出等)
  • 这个特性(在其某些实现中)将同意 TypeScript's Design Goals 中的一些东西。
dvtswwa3

dvtswwa31#

这部分问题typeof已经解决了吗?

k75qkfdt

k75qkfdt2#

对于 typeof 的对象,它给出的是一个广义类型,而不是一个确切的类型。
标量示例给出的是确切的类型,下面的示例将失败,因为 red 不能分配给 typeof color = 'blue':

const blue = 'blue';
const red: typeof color = 'red';

但是在对象中,它会给出一个泛型类型,下一个示例可以编译,因为 typeof color = {name: string} 而不是 {name: 'blue'}:

const color = {name: 'blue'};
const red: typeof color = {name: 'red'};

对于数组,下一个示例可以工作,因为 typeof collorArray = string[]:

const colorArray = ['red', 'green', 'blue'];
const newColors: typeof colorArray = ['black'];

但是还有其他类型,colorArray 可以分配给它们,我能想到的有 [string, string, string]('red' | 'green' | 'blue')[],最严格的是 ['red', 'green', 'blue'],这是唯一一个可以通过使用类型转换派生其他类型的类型。我希望这些转换已经可用了。

vsaztqbk

vsaztqbk3#

听起来你想要更频繁地使用 as const

ilmyapht

ilmyapht4#

谢谢你指出这一点,我不知道这个功能。这肯定会帮助我从模式中获取一些东西到类型。
上面的示例
这将我的期望重置为实现从模式定义类型的目标,如果我实现了这个目标,我可以发布一个包,它可能会找到用途。但是编写TypeScript并生成模式肯定更容易,因为它更易读,不像模式那样冗长。你有什么建议可以帮助我这样做吗?

b4lqfgs4

b4lqfgs45#

我有一些情况,其中as const无法完成任务。能够从单值泛型类型创建常量值将是非常棒的。它将允许利用单态性来执行动态检查。
这个例子并不是“最优”的,因为它可以使用“最大键”来减少我们访问字段的次数,甚至更好的是,它可以使用具有相同结构的对象,但在叶子节点中有布尔值或空值。无论如何,它说明了这样一个想法:

interface MyType { ... }

type RecursivePartial<T> = {
  [P in keyof T]?: RecursivePartial<T[P]>
}
type MyFlakyType = RecursivePartial<MyType>

function validateRequired<T>(arg: RecursivePartial<T>): T {

  // Except for "const", UnionToTupe and GenericKey are already resolved
  const keys = UnionToTuple<NestedKey<T>> // <-- This would be just AWESOME

  const fieldErrors: IFieldError[] = []
  keys.forEach((key) => {
    validateNestedField(arg, key, fieldErrors)
  })

  if (fieldErrors.length) {
    throw new ValidationError(fieldErrors)
  }

  return arg
}

// ------------------------------
// Appendix, utility definitions:
// ------------------------------

type UnionToTuple<T> = (
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  (T extends any ? (t: T) => T : never) extends infer U
    ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (U extends any ? (u: U) => any : never) extends (v: infer V) => any
      ? V
      : never
    : never
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
) extends (_: any) => infer W
  ? [...UnionToTuple<Exclude<T, W>>, W]
  : []
  
type NestedKeyTypeBuilder<L, T, K extends keyof T> = L extends [
  infer A,
  ...infer R
]
  ?
      | (A extends K
          ? T[A] extends Record<string, unknown>
            ? [A, ...NestedKey<T[A]>] | [A]
            : [A]
          : never)
      | NestedKeyTypeBuilder<R, T, K>
  : never

type _NestedKey<T> = NestedKeyTypeBuilder<UnionToTuple<keyof T>, T, keyof T>
export type NestedKey<T> = T extends RecursivePartial<infer T2>
  ? _NestedKey<T2>
  : _NestedKey<T>
klr1opcd

klr1opcd6#

我明白你的观点@castarco,但我也理解这对项目来说是一个非目标。这将为每种类型生成不同的函数,而且在javascript中无法工作。我们可以使用初始提案来代替,让validateRequired接收由类型生成的信息作为参数。换句话说,type to constantMap不能与泛型类型一起使用。

感谢你的留言,谁知道这可能会成为未来的一个功能呢。

相关问题