TypeScript 比你想象的还要强大
TypeScript 类型系统语言(TSts)🟦
TypeScript——通常被称为 JS 和附加的类型注解,这是描述 TypeScript 的正确方式,但却隐藏了该语言本身的潜力。如果我将 TypeScript 描述得远不止于此,而是将其描述为一个 shell 中的两种语言,会怎么样?
TypeScript 和所有静态类型语言一样,有两个级别——值级别和类型级别。值级别可以简单地理解为 JavaScript,所有语法和语法都在这个级别上工作,完全符合 JS 规范的规定。第二个级别——类型级别,是专门为 TypeScript 创建的语法和语法。TypeScript 的功能更丰富,我们可以将其分为三个级别——类型系统语言、类型注解以及 JavaScript。
注意类型注释级别是类型系统与 JS 相遇的地方,因此这些都是类似注释
a: T
和断言的地方a as T
。
本文将向您介绍TypeScript 类型系统 (TSts) ,它本身就是一门功能齐全的语言,所以请做好准备💪。
TypeScript 类型系统语言(TSts)🟦
当我们思考任何语言时,我们都会考虑值、变量、表达式、运算符和函数。我们有执行数据流的工具,我们可以根据条件分支数据流,我们可以通过迭代或递归来迭代数据流。让我们来探究一下在 TypeScript 类型系统中可以看到多少这些东西?
值和变量
在 TS 类型系统中,值代表什么?它是一种类型,值在这个级别上被表示为一种类型。我们可以使用典型的赋值运算符将值赋给变量。
// TSts🟦
type X = string;
type Y = number;
type Z = boolean;
左边是别名,即我们为值设置的名称,右边是值。变量X
有值string
。
注意,我说变量是因为变量是我们所有程序员都知道的一个术语,在 TS 中只有单个赋值变量,所以我们不能重新声明该值,自然就像 JS
const
注意: TSts 中的类型不仅类似于 const,还类似于 immutable const,因为它无法被改变。我们可以组合类型,但无法改变原始类型。
type X = 1; /* is TSts🟦 equivalent for JS🟨 : */ const X = 1;
类型的类型
在文章的下一部分中我将使用这样的术语:
type
和value
value
和type
kind
是该类型的类型
这里的类型可以是新的东西,在 TypeScript 类型系统中,类型是定义另一种类型形状的东西,就像在 JS 级别类型注释定义 JS 值形状一样。
X extends string /*is TSts🟦 equivalent for annotated JS🟨 */ const X: string
运算符
毫不奇怪,类型级别 TypeScript 有自己的运算符,下面是其中一些
A = B
分配A & B
路口A | B
联盟keyof A
A extends B ? C : D
状况K in T
迭代
// TSts🟦
type Z = X | Y // Z is either X or Y
type D = A & B | C // D is combined A and B or C
type Keys = keyof {a: string, b: boolean} // get property keys in form of union
条件和平等
正如我所写,我们可以通过条件运算符(TS 文档中称之为条件类型)来执行条件判断,那么如何检查某事物是否等于另一事物呢?为了实现这一点,我们需要理解,当我们询问时,A extends B
它意味着如果A
那么B
并且A
可以用作B
,什么在一个方向上表示相等(操作不可交换),如果A extends B
不是,则意味着B extends A
。为了检查相等性,我们需要在两个方向上执行检查。
// TSts🟦
type A = string
type B = "1"
type AisB = A extends B ? true : false // false
type BisA = B extends A ? true : false // true
正如您所见,B
可以用作,A
但不能反过来用。
// TSts🟦
type A = 1
type B = 1
type AisBandBisA = A extends B ? B extends A ? true : false : false // true
以上是完全相等性检查,我们从两个方向进行检查,然后认为类型相等。
请注意,这样实现的等式判断存在一些缺陷,在某些情况下无法正常工作。您可以在Playground中找到此类示例。特别感谢Titian Cernicova Dragomir 的评论。
注意:这种简单的实现对于联合类型也是不正确的
注意:非常值得一提的是,对于合理类型,此类条件和相等性以合乎逻辑的方式起作用,但 TS 也存在不合理的条件和相等性,例如
any, unknown, never
,这些类型可能会给我们带来相当令人惊讶的结果,并且相等性对于这些类型无法正确起作用。
功能
函数是基础抽象的基础。幸运的是,在 TS 类型系统中,有一些函数,这些函数可以处理通常被称为泛型的类型。让我们创建一个函数,用于检查任意两个值是否相等:
// TSts🟦
type IsEqual<A, B> = A extends B ? B extends A ? true : false : false
// use it
type Result1 = IsEqual<string, number> // false
type Result2 = IsEqual<1, 2> // false
type Result3 = IsEqual<"a","a"> // true
函数IsEqual
有两个参数A, B
,可以是任何类型。因此,函数可以适用于任何类型(单参数类型*
)。但我们可以创建对参数要求更精确的函数。
// TSts🟦
type GetLength<A extends Array<any>> = A['length']
type Length = GetLength<['a', 'b', 'c']> // evaluates to 3
函数GetLength
是仅当类型为 kind 时才起作用的函数Array<any>
。再看看这两个函数,如果我把它们放在 JS 函数前面,你会看到什么?
// TSts🟦
type IsEqual<A, B>
= A extends B
? B extends A
? true
: false
: false
// JS🟨
const isEqual = (a:any, b: any) => a == b ? b == a ? true : false : false
// TSts🟦
type GetLength<A extends Array<any>> = A['length']
// JS🟨
const getLength = (a: Array<any>) => a['length']
几乎一模一样,你不觉得吗?我希望现在你已经确信,流行的泛型类型只是在编译时被求值的函数💪
组合函数
如果我们有函数,那么很自然地会想到有可能在一个函数中调用另一个函数。例如,让我们重用之前编写的IsEqual
函数,并在另一个函数体中使用它IfElse
。
// TSts🟦
type IfElse<A, B, IfTrue, IfFalse> =
IsEqual<A, B> extends true ? IfTrue : IfFalse
type Result1 = IfElse<0, 1, 'Equal', 'Not Equal'> // Not Equal
type Result2 = IfElse<1, 1, 'Equal', 'Not Equal'> // Equal
局部变量
我们有函数,也有变量,但是我们可以有函数局部作用域变量吗?当然可以,至少我们可以对它们有一些想象,这很方便。
// TSts🟦
type MergePropertyValue<
A,
B,
Prop extends (keyof A & keyof B),
_APropValue = A[Prop], // local variable
_BPropValue = B[Prop]> // local variable
= _APropValue | _BPropValue // sum type
// JS🟨 take a look at similar JS function but working at assumed number fields
function mergePropertyValue(a, b, prop) {
const _aPropValue = a[prop];
const _bPropValue = b[prop];
return _aPropValue + _bPropValue; // sum
}
在参数列表的末尾,我们可以放置局部变量并赋值,这是一个很好的工具,可以为已求值的结构添加别名。在上面的例子中,我们并没有获得太多帮助,但如果类型更复杂,这样的局部别名会很方便,而且我们还可以在那里使用其他函数!让我们尝试对三个参数进行相等性检查。
// TSts🟦
type AreEqual<
A,
B,
C,
_AisB = IsEqual<A, B>,
_BisC = IsEqual<B, C>,
> = _AisB extends true ? IsEqual<_AisB, _BisC> : false
type Result = AreEqual<1,1,1> // true
type Result2 = AreEqual<1, 2, 1> // false
type Result3 = AreEqual<'A', 'A', 'A'> // true
type Result4 = AreEqual<'A', 'A', 'B'> // false
在上面的定义中_AisB
,和_BisC
可以被认为是AreEqual
函数的局部变量。
请注意,这些是局部变量的问题在于,我们可以通过设置类似的参数从外部破坏我们的类型行为
AreEqual<1, 1, 1, false, false>
。注意,上面我在局部变量上使用的下划线只是惯例。其目的是提示这些参数不应该被触碰。
循环
每种语言都有一种迭代数据结构的方法,TSts也不例外。
// TSts🟦
type X = {a: 1, b: 2, c: 3}
type Y = {
[Key in keyof X]: X[Key] | null
} // {a: 1 | null, b: 1 | null, c: 1 | null}
类型Y
是通过循环迭代for in
类型来求值的X
,我们会在每个字段上X
附加一个值null
。TSts的功能更强大,我们甚至可以直接进行迭代,比如从 0 到 5。
// TSts🟦
type I = 0 | 1 | 2 | 3 | 4 | 5
type X = {
[Key in I]: Key
}
// X is [0, 1, 2, 3, 4, 5]
// JS🟨 look at JS similar code
const x = []
for (let i = 0; i<= 6; i++) {
x.push(i);
}
我们刚刚生成的类型表示一个包含 6 个元素的数组,其值从 0 到 5。神奇的是,在类型层面,我们已经迭代了从i=0
到i=5
,并将结果推送到i
数组。看起来是for loop
不是很棒?
注意嘿读者👋,记住我们一直在谈论类型,每次我说值时它都与类型相同。0,1......上面的类型只有完全相同的 JS 运行时表示,但我们仍然只使用类型。
递归
递归是指函数定义中调用自身的一种情况。我们可以在函数体内部调用同一个函数吗?当然可以!
// TSts🟦
type HasValuesOfType<T extends object, F> = ({
[K in keyof T]: T[K] extends F ? true : T[K] extends object ? HasValuesOfType<T[K], F> : false
}[keyof T]) extends false ? false : true
上述函数HasValuesOfType
遍历参数,该参数是一种对象(类型的类型)。函数检查属性值是否具有给定的类型,如果是,则返回true
;如果不是,则递归调用自身,判断该属性是否也是对象。函数结果会告诉我们,在类型的任何层级上是否存在所需的类型。
映射、过滤和缩减
该语言具有条件、循环递归功能,让我们尝试使用这些工具来转换类型。
映射
// TSts🟦
type User = {
name: string,
lastname: string
}
type MapUsers<T extends Array<User>> = {
[K in keyof T]: T[K] extends User ? { name: T[K]['name'] } : never
}
type X = [{
name: 'John',
lastname: 'Doe'
}, {
name: 'Tom',
lastname: 'Hanks'
}]
type Result = MapUsers<X> // [{name: 'John'}, {name: 'Tom'}]
函数MapUsers
作用于用户类型的数组,并通过移除 来映射每个用户lastname
。看一下我们如何映射 - { name: T[K]['name']}
,在对类型的每次迭代中T
,我们都会获取此时的值T[K]
,并获取name
我们赋给新值的属性。
过滤
TSts为我们提供了简单过滤对象类型的工具。我们可以创建一个函数FilterField
,用于从对象类型的值中删除字段。
// TSts🟦
type FilterField<T extends object, Field extends keyof T> = {
[K in Exclude<keyof T, Field>]: T[K]
}
// book
type Book = {
id: number,
name: string,
price: number
}
type BookWithoutPrice = FilterField<Book, 'price'> // {id: number, name: string}
请注意,过滤元组类型并不是那么简单,如果没有非常复杂的技巧,语言就无法支持它,而这超出了本文的讨论范围。
FilterField
正在进行迭代T
,但通过使用Exclude
它从键列表中排除Field
,结果我们得到了没有这个字段的对象类型。
注意,有一个实用程序类型
Pick
,Omit
可以用来代替FilterField
减少
缩减或折叠是将数据从一种形状A
🍌转换为另一种形状B
🌭。我们可以这样做,并将数据从一种类型转换为A
另一种类型B
吗?当然可以😎,即使我们在之前的例子中已经这样做过了。例如,让我们计算一下对象作为参数传递了多少个属性。注意,这一点可能比较难理解,但我想在这里展示的是语言的力量:
// TSts🟦
type Prepend<T, Arr extends Array<any>> = ((a: T, ...prev: Arr) => any) extends ((...merged: infer Merged) => any) ? Merged : never
type KeysArray<T extends object, ACC extends Array<any> = []> = ({
[K in keyof T]: {} extends Omit<T, K> ? Prepend<T[K], ACC> : KeysArray<Omit<T, K>, Prepend<T[K], ACC>>
}[keyof T]);
type CountProps<T extends object, _Arr = KeysArray<T>> = _Arr extends Array<any> ? _Arr['length'] : never;
type Y = CountProps<{ a: 1, b: 2, c: 3, d: 1 }> // Evaluates to 4
是的,有很多代码,是的,相当复杂,我们需要使用一些额外的辅助类型Prepend
和KeysArray
,但最终我们能够计算对象中的属性数量,所以我们将对象从减少{ a: 1, b: 2, c: 3, d: 4 }
到4
🎉。
元组转换
TypeScript 4.0 引入了可变元组类型,这为我们的 TSts 语言级别提供了更多工具。现在我们可以非常轻松地删除、添加元素或合并元组。
// merging two lists
// TSts🟦
type A = [1,2,3];
type B = [4,5,6];
type AB = [...A, ...B]; // computes into [1,2,3,4,5,6]
// JS🟨 - the same looking code at value level
const a = [1,2,3];
const b = [1,2,3];
const ab = [...a,...b];
// push element to the lists
// TSts🟦
type C = [...A, 4]; // computes into [1,2,3,4]
// JS🟨 - the same looking code at value level
const c = [...a, 4];
// unshift element to the list
// TSts🟦
type D = [0, ...C]; // computes into [0,1,2,3,4]
// JS🟨 - the same looking code at value level
const d = [0, ...c];
我们可以看到,由于可变元组类型,TSts 上的元组操作与使用扩展语法的 JS 中的数组操作非常相似。
注意,在我们的类型级别上,元组类型可以被视为一个列表。
字符串连接
对于 TS > 4.1 版本,字符串连接也不再是问题。我们可以在类型级别粘合字符串,其方式几乎与在值级别粘合相同。
// concatenate two strings
// TSts🟦
type Name = "John";
type LastName = "Doe";
type FullName = `${Name} ${LastName}`; // "John Doe"
// JS🟨 - the same looking code at value level 🤯
const name = "John";
const lastName = "Doe";
const fullName = `${name} ${lastName}`;
那么列表中的字符串连接怎么样?
// TSts🟦
type IntoString<Arr extends string[], Separator extends string, Result extends string = ""> =
Arr extends [infer El,...infer Rest] ?
Rest extends string[] ?
El extends string ?
Result extends "" ?
IntoString<Rest, Separator,`${El}`> :
IntoString<Rest, Separator,`${Result}${Separator}${El}`> :
`${Result}` :
`${Result}` :
`${Result}`
type Names = ["Adam", "Jack", "Lisa", "Doroty"]
type NamesComma = IntoString<Names, ","> // "Adam,Jack,Lisa,Doroty"
type NamesSpace = IntoString<Names, " "> // "Adam Jack Lisa Doroty"
type NamesStars = IntoString<Names, "⭐️"> // "Adam⭐️Jack⭐️Lisa⭐️Doroty"
上面的例子可能看起来有点复杂,但证明我们可以拥有通用类型级别函数来将字符串与给定的分隔符连接起来。
高阶函数?
TSts是函数式语言吗?可以传递函数并返回函数吗?下面是一个简单的 try 示例。
// TSts🟦
type ExampleFunction<X> = X // identity function
type HigherOrder<G> = G<1> // 🛑 higher order function doesn't compile
type Result = HigherOrder<ExampleFunction> // 🛑 passing function as argument doesn't compile
不幸的是(或者幸运的是)没有这样的选择,在类型级别上这种东西有一个名字 -更高种类的类型,这样的构造可以在例如 Haskell 编程语言中使用。
这也意味着我们不能创建像 map、filter 和 reduce 这样的多态函数,因为这些函数构造需要类型* -> *
(函数)作为参数。
标准库
每种语言都有一些标准库,TypeScript 的类型级语言也一样。它有一个标准库,在官方文档中被称为“实用程序类型”。尽管名称如此,但实用程序类型实际上是 TypeScript 内置的类型级函数。这些函数可以帮助进行高级类型转换,而无需从头编写所有内容。
总之
TypeScript 类型系统TSts应该被视为一种功能齐全的语言,它具备任何语言应有的一切:变量、函数、条件、迭代、递归,可以组合,可以编写复杂的转换。类型系统基于表达式,并且仅对不可变值(类型)进行操作。它没有高阶函数,但这并不意味着它不会有 😉。
其他链接:
如果您想了解 TypeScript 及其相关有趣的事情,请在dev.to和twitter上关注我。
文章来源:https://dev.to/macsikora/typescript-is-more-than-you-think-2nbf