高级TypeScript类型技巧

2020-12-23 21:12:18

在TypeScript中花了一些时间之后,您可能会开始渴望生活中到处的类型安全。 JavaScript本质上是一种不安全的语言,几乎在每个框架中到处都有龙。大多数框架已被充分介绍,但是如果您在内部设计一个漂亮的API,那么它需要具有令人难以置信的功能丰富和令人难以置信的安全性,但仍需要最好的JavaScript习惯用法,该怎么办。那么到那时,您可能不得不投入一些漂亮的粗糙类型的编写。

通常,这里有许多TypeScript TYPE库,例如ts-toolbelt,实用程序类型,type-fest等。这些库很棒,并且包含大量信息,但是在描述它们创建实用程序类型的方式时,它们通常很少。

有很多很棒的类型库,但是没有很好的关于它们如何构建的文档

这篇文章的目的不仅是公开一些出色的实用程序类型,而且还包括我在开发过程中意识到的一些技巧。不幸的是,这篇文章需要对TypeScript类型和技巧有相对深入的了解。高级类型是潜水之前开始的好地方!

其中许多解决方案(和问题)可能会让人感到深奥,这是因为坦率地说它们确实如此。它们不是为胆小者准备的,不需要为日常使用TypeScript而闻名。这是为难以置信的复杂或不确定的JavaScript问题编写类型库的现实。

这些类型中的大多数来自一些正在使用的TypeScript库,即安全模式和mongo-safe,我打算在以后对这两种类型进行冗长的博客文章。

我已尝试通过用法示例以及问题或解决方案新颖或有趣的原因来组织这些工作。它们没有特定的顺序。

//优质鞋类< & =花式<亮度<深深地<嵌套& >>>扩展推断TItem吗? {item:TItem} | TItem | [TItem,TItem] | [TItem]:不可能;类型Impossible = never;

有时,您将需要在类型定义中反复使用相同的类型。每次TypeScript遇到您的类型时,都必须再次对其进行评估。使用推断技巧,您可以将变量存储在类型中,以便在其余所有变量中使用。

您将在几个地方看到此扩展推断技巧。推断具有一些有趣的属性,使其在定义复杂类型时很有用。

注意此处使用Impossible。我们永远无法到达“不可能”分支,因此通常我们在这里不会指定“永不分支”,但我发现,将来再次查看这些类型时,只要再考虑“永不分支”,就永远不要把“永不存在”混淆。使用可解析为基本类型的显式类型别名是在定义中显式的一种好方法。

注意:如果您输入的结果永远不会,则此方法将无效。如果T或Fancy< Shumancy< Dested< Nested<>。永远不会,整个表达式将永远不会。

//错误类型的鞋子< & = T永远不会延伸? 1:0;类型Test1 =鞋子< ' hi' &gt ;; // 0 type Test2 = Shoes<永不&gt ;; //从不

//优质鞋类< & = [T]延伸[从不]? 1:0; // ^ ^ ^ ^ ^类型Test1 =鞋子< ' hi' &gt ;; // 0 type Test2 = Shoes<永不&gt ;; // 1

要永远测试都不容易。一旦表达式中的一种类型从不存在,它将毒化表达式的其余部分,使其评估为永不。通常,这是一个有用的功能,但是当您实际上不需要检查时,可以将表达式包装在元组中。这将使TypeScript编译器欺骗苹果,而不是扩展从不。

它通过将元组T与永不比较元组来工作。我们在许多地方都使用了这个元组把戏。除了元组使TypeScript在评估时不太贪婪之外,我不确定它为什么起作用。

//错误类型的鞋子< T1,T2,T3> = T1扩展了{}? (T2扩展数字?(T3扩展字符串?1:0):0):0;

//优质鞋类< T1,T2,T3> = [T1延伸{}? 1:0,T2扩展号码? 1:0,T3扩展了什么? 1:0]扩展[1,1,1]? 1:0;

您将经常需要深入检查一些事情,并且仅在所有值都是确定值时才执行一些逻辑。这可能会导致令人讨厌的深层嵌套结构。

一个干净的解决方案是在一个元组中预先检查所有这些对象,并将其与每个对象的预期结果进行比较。现在您的树只有一层深,您的意图更加清晰了!

//错误的类型StringToObject< & = T延伸字符串? {[T中的键]:布尔值}:从不;类型UnionToObject< & = StringToObject< T&gt ;;类型Nested< & = {[T的键]:UnionToObject< T [key]>}; const value:嵌套< {thing:' a' | ' b' | ' c'}> { //没有错误

//好的类型StringToObject< & = [T]扩展[字符串]? {[T中的键]:布尔值}:从不; //类型^ UnionToObject< & = StringToObject< T&gt ;;类型Nested< & = {[T的键]:UnionToObject< T [key]>}; const value:嵌套< {thing:' a' | ' b' | ' c'}> { //错误缺少' b'

有时您会非常深地通过工会,以至于工会到达其最终目的地时,工会已被分发。如上所述,这可能导致意外的行为。由于联合已经分发,因此第一个示例中的value将导致类型

解决方案是将T封装在一个元组中,以强制TypeScript不在后续表达式中分配联合。

// Bad type Bad = {shoes:never;其他:布尔值}常量值:错误= {鞋子:1,其他:true}; //在鞋子上抛出错误,但智能感知和类型误导

//良好的导出类型ExcludeNever< & = {[T中的key作为T [key]永远不会扩展?从不:key]:T [key];}; const值:ExcludeNever<不好= {鞋:1,其他:true} //引发完全错误并且没有智能提示

有时,您将处理Record中的某些类型并将值设置为Never。这是完全有效的,并且当用户尝试在其中输入值时会抛出错误。

问题是,intellisense不会将此键从结果列表中排除。确保类型永远不会,但是由于键仍然存在,因此您可以在其中放置一个值。

解决方案是使用新的TypeScript作为语法,在适当的时候将密钥设置为never,这样用户将无法使用它。

注意:最终结果是相同的,您不能为鞋子添加值,但是第二种情况下DX有了显着改进。

现在,这一点有点难以解释,但是很容易成为本文中最强大的技巧之一。它节省了我的培根太多次了。

上面的示例是故意稀疏的,因为很难找到真实的示例。在需要时,您会知道的。

在使一个复杂的库完全具有类型安全性的过程中,不可避免地会创建一个潜在地无限嵌套的类型。

有一点背景:TypeScript的规则是,在举起手来解决可怕的“类型实例化过深甚至可能无限”之前,它通常不会深入到大约50个级别的类型检查。这意味着您的类型过于复杂,需要花费很长时间进行评估,因此不会。

我会说这90%的时间是由于您的代码中的一个问题可以解决的,而无需使用欺骗手段。通常,您在某个地方搞砸了。但是,在极少数情况下,您可以正确地进行所有操作,可以对类型进行适当的优化,但是实际上,您的库要求确实可以解决问题,您可以使用上述技巧。

它的工作方式对我来说还是个谜。这是我从令人难以置信的ts-toolbelt库中学到的技巧。我能说的最好的是,它推迟了对类型T的求值,直到真正需要它为止。这意味着只是因为您的类型可以无限嵌套,所以将根据用法而不是总括语句来测试此结果。

也就是说,如果您传递的T确实耗尽了50个上限,那么TypeScript一定会让您知道,但是在此之前,所有表现良好的T都将继续起作用。

我不知道上面的解释是否正确,不希望被更正!根据我的经验和研究,这就是它的工作原理。

类型深度= 1 | 2 | 3 | 4 | 5;输入NextDepth< TDepth扩展深度。 = TDepth扩展1? 2:TDepth扩展2? 3:TDepth扩展3? 4:TDepth扩展4? 5:从不;导出类型SafeTypes =数字|字符串布尔|日期导出类型DeepKeys< T扩展{},TDepth扩展深度= 1>。 = TDepth扩展5? // ^^这是要检查的'' :{{[keyof T中的键]:键扩展字符串? T [key]扩展SafeTypes吗? `$ {key}`:T [key]扩展{}? `$ {key}`| | `$ {key}。 $ {DeepKeys< T [key],NextDepth< TDepth> > }`:// ^^这就是魔法never:never; };

这是一个现实的例子,说明这个问题可能并不常见,但我遇到了多次。使用新的TypeScript 4.1字符串模板语法(这是令人难以置信的),您现在可以生成代表深层嵌套对象的字符串联合。

问题是TypeScript中的并集只能有这么多成员,因此根据对象的复杂程度和深度嵌套,您可能会浪费掉这个数量。

上面提出的解决方案远非理想,随着使用量的增加最终会中断,但是对于如何构建类型系统,这是值得思考的更多内容。

我在这里选择的折衷办法只是限制嵌套。我决定,开发人员必须自己决定,我最多只能保持5级安全。这适用于我的一个项目的一个用例,但是您的工作量可能会有所不同!

/ *设置* /类型SomeType = / ** /;函数doThing< TTable,TLookupKey>(参数:{tableName:字符串,键:TLookupKey}):TTable; / *烦人* / doThing< SomeType,'鞋子({key:' shoes&#39 ;, tableName:' Shoe'}); // // ^ ^必须同时指定:-( / *理想* / doThing< SomeType>({key:' shoes&#39 ;, tableName:' Shoe'}); //仅^ / * Magical Compromise * /类型TableName< TTable> =字符串& {__table:TTable};导出函数tableName< TTable扩展{}>(tableName:string):TableName< TTable> {返回tableName作为TableName< TTable&gt ;;}函数doThing< TTable,TLookupKey>(参数:{tableName:TableName< TTable&gt ;,键:TLookupKey}):TTable; doThing({key:' shoes&#39 ;, tableName:tableName< SomeType> (' Shoes')}); // ^均不指定!

好吧,也许这是一个延伸,但我个人认为这是这里最酷的把戏,当我意识到自己的潜力正在陷入僵局时。

请参阅,TypeScript将根据使用情况推断通用类型,但仅当可以推断所有类型时。如果不能,则必须自己提供所有类型。充其量这可能是一件繁琐的事情,更糟的是这是不可能的。

我们这里的解决方案是使用一个实际上只是一个字符串的不透明类型,但使用它来携带一些额外的类型数据,然后我们将这些数据作为tableName参数传递。这使TypeScript具有足够的类型信息来正确地推断TTable,而不会迫使我们手动提供类型!

在此过程中,我创建或策划了一些我很少看到的有用类型,或者至少解释了它们为何有用。

尽管它看起来很明显且人为设计,但有时您还是想在一个您有信心的类型上查找一个键,即使TypeScript不确定。解决方案是检查该键是否在您的类型中存在,但这需要您再嵌套一次表达式,这充其量会让您感到烦恼,最糟糕的是可能会使您的50个输入深度变大。解决方案是使用简单的Lookup类型,该类型在其无效时从不返回。

与上述相同,当您拥有一个知道可以扩展另一种类型的类型,但是您不在TypeScript同意而无需断言的地方时,Cast将为您完成工作而不嵌套您的表达式。

类型DiscriminateUnion< T,TField扩展T的键,TValue扩展T [TField] = T扩展了{[TField中的字段]:TValue}? T:从不;类型Query = {类型:' a&#39 ;,数字:1} | {类型:' b'字符串:' 1' };类型OnlyTypeA = DiscriminateUnion<查询,类型' a'>

这是另一种典型的方法,您需要时才需要。传统观点认为,仅将联合的每个部分存储为单独的类型然后从那里去,但这并不总是很方便或不可能的。这种类型将使您仅拥有一部分工会的具体版本。

类型LinkedList< T,Last = never> = T扩展为只读[推断头,...推断尾巴]? {Item:头;下一个:LinkedList<尾巴,头>}:从不;类型LinkedListReverse< T扩展了只读unknown [],Last = never> = T扩展为只读[推断头,...推断尾巴]? LinkedListReverse<尾巴,{项目:头部; Last:Last}> :{Last:Last;项目:从不};

类型LinkedListResult = LinkedList< [{{$ match:{someId:' ah'}},{$ project:{someValue:' $ someId'}},{$ group:{_id :' $ someValue'}},{$ sort:{someValue:1}}]&gt ;; / * {项目:{$ match:{someId:' ah&#39 ;; }; };下一步:{项目:{$ project:{someValue:' $ someId&#39 ;; }; };下一个:{项目:{$ group:{_id:' $ someValue&#39 ;; }; };下一步:{项目:{$ sort:{someValue:1; }; };下一步:永不; }; }; }; } * / //现在,您可以像这样遍历链接列表:键入Process< T,TValue> = / *?* /类型TraverseLinkedList< T,LNL下一个> = [LookupKey< LNLNext,' [Next]扩展[从不]?处理<查找键< LNLNext,'项目'> > :TraverseLinkedList< T,LookupKey< LNLNext,'下一个>扩展推断J?处理< J,LookupKey< LNLNext,'项目'> > :不可能;类型Result = TraverseLinkedList< {},LinkedListResult;

从前,我需要将元组转换为LinkedList,以简化元组数组的处理。我不希望这对任何人都有用,但这是一个不错的练习,可能会迫使您以稍微不同的方式考虑构建类型。

这有一个问题,即您的元组中可能限制为大约30个项目(50个限制减去TraverseLinkedList中的嵌套)。结果,我没有将它用于解决方案,因为我不想限制用户。

我希望这些提示和技巧对您有所帮助。 我希望这会引起更多的类型工程师来记录构建复杂的TypeScript类型的令人费解的难题。 我希望在以后的文章中记录一些调试复杂类型的方法,并深入探讨如何使MongoDB聚合类型安全。 如果您有任何疑问或意见,请随时在Twitter上与我联系!