如何花两天时间多次重写同一代码的故事。

参赛作品
在本文中,我将省略有关Hapi,Joi,路由和validate: { payload: ... }
详细信息validate: { payload: ... }
,这意味着您已经了解它的含义以及术语,例如“接口”,“类型”等。 。 在这些方面的训练,我只会告诉您一个回合制策略,而不是最成功的策略。
一点背景
现在,我是该项目上唯一的后端开发人员(即,编写代码)。 功能不是本质,但是关键本质是对个人数据的较长描述。 代码的速度和质量是基于我很少从头开始独立处理项目的经验,甚至没有使用JS的经验(仅第4个月),并且一路走来非常歪斜,我是用TypeScript编写的(以下简称TS)。 日期被压缩,滚动被压缩,编辑不断到达,结果证明首先要编写业务逻辑代码,然后是最上面的接口。 然而,技术责任能够赶上并敲击瓶盖,这大约发生在我们身上。
在进行了3个月的项目工作后,我终于与同事们同意切换到单个词典,以便该对象的属性在任何地方都被命名和编写相同的内容。 当然,在这项工作下,我承诺编写一个界面,并紧紧地坚持了两个工作日。
问题
一个简单的用户配置文件将是一个抽象示例。
首先 优秀开发人员的零步骤: 描述数据 编写测试;- 第一步:
编写测试 描述数据 - 等等。
假设已经为该代码编写了测试,但仍然需要描述数据:
interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' };
好吧,这里的所有内容都很清楚而且非常简单。 我们记得,所有这些代码都是在后端,或者在api中,也就是说,用户是基于网络上的数据创建的。 因此,我们需要验证传入的数据并在以下方面帮助Joi:
const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
解决方案“在额头上”已准备就绪。 这种方法的明显缺点是验证器与接口完全分离。 如果在应用程序的生命周期中字段更改/添加或字段类型更改,则需要手动跟踪此更改并在验证器中指出。 我认为,除非有任何问题,否则不会有这样负责任的开发人员。 此外,在我们的项目中,调查问卷由三个嵌套级别的50多个字段组成,要了解这一点非常困难,即使是全心全意地了解。
我们根本无法指定const joiUserValidator: IUser
,因为Joi
使用其数据类型,当编译类型Type 'NumberSchema' is not assignable to type 'number'
类型时,它会产生错误。 但是必须有一种在接口上执行验证的方法吗?

也许我没有正确搜索它,或者对答案的研究不力,但是所有的决定都extractTypes
于extractTypes
和某种凶猛的自行车,例如:
type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : never;
解决方案
使用第三方库
为什么不呢 当我向人们询问我的任务时,我收到了一个答案,后来又在这里的评论中(感谢keenondrums )链接到这些库:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer
但是,有兴趣自己弄清楚,以更好地理解TS的工作,并且没有人敦促立即解决问题。
获取所有属性
由于我以前没有使用过静态函数,因此以上代码在类型中使用三元运算符方面发现了America。 幸运的是,不可能将其应用到项目中。 但是我发现了另一辆有趣的自行车:
interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
在相当棘手和神秘的条件下, TypeScript
允许您从接口中获取键,就好像它是普通的JS对象一样,但是,只能在type
构造中以及通过key in keyof T
以及仅通过泛型来获取。 作为UserKeys
类型的结果,实现接口的所有对象应具有相同的属性集,但是值的类型可以是任意的。 这包括IDE中的提示,但仍未明确指示值的类型。
这是另一个我无法使用的有趣案例。 也许您可以告诉我为什么这样做是有必要的(尽管我部分猜测,但是没有足够的应用示例):
interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
使用变量类型
您可以显式设置类型,并使用“ OR”并提取属性,以获取本地工作代码:
type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
当我们想从数据库中拾取有效对象时,就会出现此代码的问题,也就是说,TS事先不知道哪种类型的数据将是简单数据还是Joi。 尝试在期望为number
的字段上执行数学运算时,这可能会导致错误:
const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10;
此错误来自Joi.NumberSchema
因为age不仅可以是number
。 他们为之奋斗并遇到的。
将两种解决方案合二为一吗?
在这一点上,工作日正在接近其逻辑结论。 我屏住呼吸,喝了咖啡,抹去了他妈的。 有必要少读这些书,以减少您的互联网! 时间到了 a弹枪 洗脑:
- 一个对象必须由显式的值类型组成。
- 您可以使用泛型将类型放入单个接口中。
- 泛型支持默认类型;
type
构造显然具有其他功能。
我们使用默认类型编写通用接口:
interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; }
对于Joi,您可以创建第二个接口,以这种方式继承主要接口:
interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {}
这还不够好,因为下一个开发人员可以轻松地扩展IUserJoi
或更糟。 一个更有限的选择是获得类似的行为:
type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>;
我们尝试:
const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
UPD:
要结束在Joi.object
我必须解决错误TS2345
,并且最简单的解决方案是as any
。 我认为这不是一个关键的假设,因为上述对象仍在界面上。
const joiUserInfo = { info: Joi.object(joiUser as any).required() };
它可以编译,在使用位置看起来很整洁,并且在没有特殊条件的情况下始终会设置默认类型! 美女...

...我花了两个工作日
总结
从这一切可以得出什么结论:
- 显然,我没有学习如何找到问题的答案。 当然,有了成功的请求,此解决方案(甚至更好)将出现在搜索引擎的前5k链接中;
- 从动态转变为静态思维并非易事,更多的是,我只是对这种蜂拥而至。
- 泛型很酷。 在Habr和stackoverflow已满
自行车 在运行时之外构建强类型的非显而易见的解决方案。
我们赢了:
- 更改接口时,包括验证程序在内的所有代码都会丢失;
- 在编辑器中,有关编写验证器的属性名称和对象值类型的提示;
- 出于相同目的,缺乏晦涩的第三方库;
- Joi规则仅在必要时才适用,在其他情况下为默认类型;
- 如果某人想更改属性的值类型,则使用正确的代码组织,他将去到与该属性关联的所有类型都聚集在一起的地方。
- 我们学会了将美丽的泛型简单地隐藏在
type
抽象的后面,从视觉上从怪异的结构中卸载代码。
道德:经验是无价的;在其他方面,有一张世界地图。
您可以看到,触摸并运行最终结果:
https://repl.it/@Melodyn/Joi-by-interface
