├── .gitignore ├── README.md ├── README.zh-Hans.md ├── imgs └── as_any_as_T.png └── zh-Hans ├── Type variable define.md ├── What can TypeScript do?.md ├── What is TypeScript?.md ├── What type? - TypeScript boundary - Narrow type.md ├── What type? - TypeScript boundary.md └── What type?.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Temp files 2 | .temp.tsconfig.* 3 | 4 | # IDE 5 | .vscode 6 | .idea 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | bin 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Incomplete Guide 2 | 3 | | [zh-Hans](./README.zh-Hans.md) 4 | -------------------------------------------------------------------------------- /README.zh-Hans.md: -------------------------------------------------------------------------------- 1 | # TypeScript 不完全指南 2 | 3 | 如何在 TypeScript 类型系统中进行类型编程的不完全指南,本指南包括了大量 🔞 内容,未成年请勿阅读(~~别卷了,求求~~)。 4 | 5 | ## 目录 6 | 7 | * [什么是 TypeScript ?](./zh-Hans/What%20is%20TypeScript%3F.md) 8 | * [TypeScript 可以做什么?](./zh-Hans/What%20can%20TypeScript%20do%3F.md) 9 | -------------------------------------------------------------------------------- /imgs/as_any_as_T.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NWYLZW/TypeScript-Incomplete-Guide/4d23f19d6895421d88ff23054e856ae84d655d4a/imgs/as_any_as_T.png -------------------------------------------------------------------------------- /zh-Hans/Type variable define.md: -------------------------------------------------------------------------------- 1 | ## “变量”声明 2 | 3 | 你以为我要教你 type 和 interface 是什么?那就大错特错了!来吧,写点不一样的东西。 4 | 5 | ## `infer` 的妙用 6 | 7 | ```typescript 8 | // 当一个类型无法通过简单的方式访问到时, 我们可以使用 infer 关键字来实现对其的快速访问 9 | type Type0 = T extends ( 10 | (...args: infer U) => any 11 | ) ? U : never 12 | 13 | type P = Type0<(a: number, b: string) => void> 14 | // ^? type P = [a: number, b: string] 15 | 16 | interface Foo { 17 | a: { b: { c: T } } 18 | } 19 | type Type1 = T extends ( 20 | Foo 21 | ) ? U : never 22 | 23 | type Q = Type1<{ 24 | // ^? type Q = string 25 | a: { b: { c: string } } 26 | }> 27 | 28 | // 同时我们也可以将其运用于数组之中(虽然它可以通过简单的方式访问到) 29 | type Type2 = T extends [infer T0, ...any[]] 30 | ? T0 31 | : never 32 | 33 | type R = Type2<[1, 2, 3]> 34 | // ^? type R = 1 35 | ``` 36 | 37 | 同时在较高版本 infer 提供了可以约束推断类型的行为(infer 默认推断的类型不可用)。 38 | 39 | * [ts@4.7 - 为 `infer` 提供 `extends` 进行类型约束](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#extends-constraints-on-infer-type-variables) 40 | * [ts@4.8 - 增强模版字符串中的 `infer` 类型约束](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-8.html#improved-inference-for-infer-types-in-template-string-types) 41 | 42 | 第一个增强没啥太大的用,主要用于简化代码,以及更好的提供对 infer 的类型支持,重点是第二个可以用来整点点小活。 43 | 44 | ```typescript 45 | type T0 = "1" extends `${infer T extends number}` ? T : never 46 | // ^? type T0 = 1 47 | type T1 = "true" extends `${infer T extends boolean}` 48 | // ^? type T1 = "This is true" 49 | ? ([T] extends [true] ? 'This is true' : 'This is false') 50 | : never 51 | ``` 52 | 53 | > 其实它很像一个概念「[Pattern Matching(模式匹配)](https://zh.wikipedia.org/wiki/%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D)」,或者说就是。 54 | > 55 | > 当然在 ECMAScript 中也有一个相关的[提案](https://github.com/tc39/proposal-pattern-matching)。 56 | > 57 | > 拓展阅读: 58 | > * [Type inference in conditional types](https://github.com/Microsoft/TypeScript/pull/21496) 59 | 60 | ## 类型中的泛型 61 | 62 | 除了 infer 的形式声明一个“变量”,还有常见于在「Generic(泛型)」中的“变量”声明。 63 | 64 | ```typescript 65 | type T0< 66 | A, 67 | // 直接定义间接类型变量(这里并没有描述为「Constraint(约束)」,因为主要想阐述的是在这里的作用,你能这么用) 68 | B extends A, 69 | // 同时支持定义 type 一样的运算 70 | C extends A extends [infer A0] ? A0 : never, 71 | // 下面俩种不会保证类型一定正确,所以在后续的类型运算过程中可能会有问题 72 | D = A, 73 | E = A extends [infer A0] ? A0 : never, 74 | > = {} 75 | ``` 76 | 在这里的 type 或者 interface 中的泛型声明,可以在后续的类型运算中使用。但是如果未给定默认值,那么在调用的时候必须传入,并不是很方便。所以这里如果需要让 TypeScript 对其进行计算,那么最好给定一个与 extends 约束相同的默认值(或者 extends 定义一个相对宽泛的类型,在默认值的位置定义更确切需要的类型)。 77 | 78 | 通过上面的描述,我们可以感觉这种定义的方式似乎并不是很方便,那么我们有没有更符合的场景呢? 79 | ```typescript 80 | // 在 TypeScript 的函数中,通过编译器对参数的类型推断,我们可以不去定义参数类型,而是由编译器推断出来 81 | declare function foo< 82 | T extends readonly any[], 83 | N extends T[0] 84 | >(t: T): N 85 | 86 | const a = foo([1, 2, 3] as const) 87 | // ^? const a: 1 88 | ``` 89 | 这样看起来就方便了许多了,但是对于这个同时也存在一些已知的问题。当你想传入一个类型,但是不想传入一个类型的时候,则会导致你必须像使用 type 一样去设置一个默认值。 90 | 91 | ```typescript 92 | const b = foo<(1 | 2)[]>([1, 2, 3]) 93 | // ^^^^^^^^^ TS2558: Expected 2 type arguments, but got 1. 94 | ``` 95 | 96 | 这个问题在 TypeScript 中是暂时无法解决的,因为它是一个[已知的问题](https://github.com/microsoft/TypeScript/issues/20122),并被准备[在 5.2 版本](https://github.com/microsoft/TypeScript/issues/54298#:~:text=Investigate%20Type%20Argument%20Placeholders)中得到[支持](https://github.com/microsoft/TypeScript/pull/26349)。 97 | 98 | > 我们可以利用这个做一些常用类型的初始化,但是请注意,尽量减少在这里的类型定义。 99 | > 100 | > 在这里过多过大的类型可能会导致类型不方便被观测检察。 101 | 102 | 103 | 104 | 105 | 106 | ## 类型中的 PropertyKey 107 | 108 | 其次便是一个相对来说十分常用的类型定义位置了,在 `{}` 的 PropertyKey 的位置中我们能够使用 `in` 运算符定义一个 `string | number | symbol` 类型的变量,并在 PropertyValue 的位置中使用它。 109 | 110 | ```typescript 111 | // 在这里我们可以将 K 上的 number 形式的字符串通过 infer 转换为对应的 number 类型 112 | // _? type T0 = { 1: 1 } 113 | type T0 = { 114 | [K in '1']: K extends `${infer T extends number}` ? T : never 115 | } 116 | ``` 117 | 118 | 我们还可以使用 [ts@4.1 - Key Remapping via `as`](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as) 从而修改 PropertyKey 的名称,而不改变 PropertyValue 中对 `in` 操作符得到的类型进行变动。 119 | ```typescript 120 | // _? type T0 = { 2: 1 } 121 | type T1 = { 122 | [K in '1' as '2']: K extends `${infer T extends number}` ? T : never 123 | } 124 | ``` 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /zh-Hans/What can TypeScript do?.md: -------------------------------------------------------------------------------- 1 | # TypeScript 可以做什么? 2 | 3 | ## 类型运算 4 | 5 | 在 TypeScript 中我们可以将类型理解为一个具备一定特征的集合,该集合可以按照 TypeScript 中预定义的规则进行运算,我们可以通过传入的类型通过一系列的运算去得到一个新的类型,这就是类型运算。 6 | 7 | * [“变量”声明](./Type%20variable%20define.md) 8 | * [什么“变量”?](./What%20type%3F.md) 9 | -------------------------------------------------------------------------------- /zh-Hans/What is TypeScript?.md: -------------------------------------------------------------------------------- 1 | # 什么是 TypeScript ? 2 | 3 | **不知道。** 4 | 5 | > 这里不会去介绍任何关于 TypeScript 过于基础的信息,主要讲解一些在使用过程中需要注意的问题以及一些奇淫巧技。 6 | > 7 | > 如果你想在这里知道 TypeScript 是什么?在这里你无法找到答案,建议阅读[《The TypeScript Handbook》](https://www.typescriptlang.org/docs/handbook/intro.html)。 8 | -------------------------------------------------------------------------------- /zh-Hans/What type? - TypeScript boundary - Narrow type.md: -------------------------------------------------------------------------------- 1 | # Narrow Type 2 | 3 | 在[介于 TypeScript 与 JavaScript 之间](./What%20type%3F%20-%20TypeScript%20boundary.md)中我们提到了一些关于 TypeScript 的类型边界中的 `as const` 的作用,并了解到了它的一些特性,但是在这里我们可以根据一些 TS 中的特性来自己实现 `as const` 的功能。 4 | 5 | ## 麻烦的 `as const` 6 | 7 | 对于我们常见的 primitive type 来说,当我们在函数的 Generic 位置进行声明,并在参数位置进行使用的时候,TypeScript 会自动推断出这些类型。 8 | ```typescript 9 | declare function f0(g: G): G 10 | 11 | let t0 = f0(1) 12 | // ^? let t0: 1 13 | let t1 = f0('1') 14 | // ^? let t1: "1" 15 | let t2 = f0(true) 16 | // ^? let t2: true 17 | ``` 18 | 19 | 但是我们在使用 `object literal` 的时候,TypeScript 会自动推断出这个类型的最宽泛的类型,这个时候我们就需要使用 `as const` 来进行类型的缩小。 20 | ```typescript 21 | declare function f0(g: G): G 22 | 23 | let t0 = f0({ a: 'test' }) 24 | // ^? let t0: { a: string; } 25 | let t1 = f0({ a: 'test' } as const) 26 | // ^? let t1: { readonly a: "test"; } 27 | ``` 28 | 29 | ## 找个出路 30 | 31 | 虽然我们可以通过显式的声明来达到这个效果,但是这样的话就会显得很麻烦。那么我们有没有别的办法呢? 32 | 首先我们可以发现 TypeScript 其实在面对 `object literal` 的时候,会根据实际参数的类型信息尽可能将类型进行缩小,也就是说实际的类型并没有在传递过程中丢失成宽泛的类型。 33 | ```typescript 34 | declare function foo(x: { foo: string, bar: 1 }): typeof x 35 | 36 | const x = foo({ foo: 'foo', bar: 1 }) 37 | // ^? const x: { foo: string; bar: 1; } 38 | ``` 39 | 40 | 我们可以看到,`x` 的类型并没有丢失,那么我们尝试通过这种方式来使类型来进行缩小看看。 41 | ```typescript 42 | declare function foo(x: { 43 | [K in string]: (typeof x)[K] 44 | }): typeof x 45 | 46 | const x = foo({ foo: 'foo', bar: 1 }) 47 | // ^? const x: { [x: string]: unknown; } 48 | // 在上面我们尝试去使用 `typeof x` 来对我们需要的类型进行推断 49 | // 然而我们发现这样的话并没有起到我们想要的效果(全变成 unknown 了) 50 | // 我们可以回忆一下我们最开始的函数为什么能出现自动推断 literal 的效果 51 | // 然后我们便可以发现,我们缺少了一个 generic 类型,那么我们便可以尝试加上这个 generic 类型 52 | 53 | declare function foo0(x: { 54 | [K in keyof T]: T[K] 55 | }): [typeof x, T] 56 | 57 | const x0 = foo0({ foo: 'foo', bar: 1 }) 58 | // ^? const x0: [{ foo: string; bar: number; }, { foo: string; bar: number; }] 59 | // 我们可以看到,我们的类型已经被缩小了,但是我们的类型还是有一些问题 60 | // 那么在哪里出现了问题呢? 61 | // 在这里我猜测 TypeScript 在发现类型并不复杂的时候并不会去进行类型的缩小(可能是一个优化) 62 | // 所以我认为 TYpeScript 在我们需要进行一个类型运算的时候,它会将一个具体的类型塞进去进行运算 63 | // 那么我们便可以去尝试诱导一下它 64 | 65 | declare function foo1(x: { 66 | [K in keyof T]: T[K] extends (string) ? T[K] : never 67 | }): [typeof x, T] 68 | 69 | const x1 = foo1({ foo: 'foo', bar: 1 }) 70 | // ^? const x1: [{ foo: "foo"; bar: never; }, { foo: "foo"; bar: number; }] 71 | ``` 72 | 计划通!接下来我们通过工程化的手段来对他进行封装与优化,首先我们将类型扩充到所有的基础类型。 73 | 74 | ## 优化一下 75 | ```typescript 76 | type Primitive = string | number | boolean | bigint | symbol | undefined | null 77 | 78 | declare function foo(t: { 79 | [K in keyof T]: T[K] extends Primitive ? T[K] : never 80 | }): [typeof t, T] 81 | 82 | const x0 = foo({ foo: 'foo', bar: 1, baz: true, qux: 20n, qor: null, bor: undefined }) 83 | // ^? const x0: [{ foo: "foo"; bar: 1; baz: true; qux: 20n; qor: null; bor: undefined; }, { foo: string; bar: number; baz: boolean; qux: bigint; qor: null; bor: undefined; }] 84 | ``` 85 | 86 | 但是我们可以显然发现他不支持嵌套的定义方式,那么我们可以通过递归的方式来进行处理。 87 | ```typescript 88 | type Primitive = string | number | boolean | bigint | symbol | undefined | null 89 | 90 | type Narrow = { 91 | [K in keyof T]: T[K] extends Primitive ? T[K] : Narrow 92 | } 93 | 94 | declare function foo(t: Narrow): T 95 | 96 | const x0 = foo({ foo: 'foo', bar: { baz: true } }) 97 | // ^? const x0: { foo: "foo"; bar: { baz: true; }; } 98 | // 我们再来尝试一点特殊的类型 99 | const x1 = foo([1, 2, true]) 100 | // ^? const x1: (true | 1 | 2)[] 101 | ``` 102 | 103 | ### 啊?元组 104 | 我们可以看到类型已经被缩小了,但是没有维持住 tuple 的类型,而是丢失了元素顺序的一个 array。那么我们有没有什么办法来维持住这个行为呢?在这里我们回忆一下我们上面所做的,实际上就是去诱导 TypeScript 的隐式推断,所以我们能不能同样的诱导出 tuple 的类型呢? 105 | ```typescript 106 | // 我们可以先看一下这段代码 107 | declare function t(t: T): T 108 | 109 | const t0 = t([1, 2]) 110 | // ^? const t0: [number, number] 111 | // 我们可以发现,当我们企图让类型可能(union)是一个 empty tuple 的时候 112 | // TypeScript 会尝试将它理解(隐式推断)为一个 tuple 类型,从而保持了一个更小的类型 113 | ``` 114 | 数组其实也算是一种特殊的 literal object,所以我们可以尝试去将它转换为一个 object,然后再去进行类型的缩小。 115 | ```typescript 116 | type A0 = { 0: 1, 1: 2, length: 2 } 117 | type A1 = [1, 2] 118 | ``` 119 | 我们可以看到,数组其实就是一个带有数字索引的 literal object ,并且再附带了一些特殊的属性(这里的描述并不是特别的准确,但是可以按照这个思路理解这块的内容)。 120 | 121 | 接下来我们的事情便好办了,结合上面俩部分的内容,我们便可以很容易能得到一个这样的类型构造器。 122 | ```typescript 123 | type Primitive = string | number | boolean | bigint | symbol | undefined | null 124 | 125 | type Narrow = T extends [] ? [] : { 126 | [K in keyof T]: T[K] extends Primitive ? T[K] : Narrow 127 | } 128 | 129 | declare function foo(t: Narrow): T 130 | 131 | const x0 = foo({ foo: 'foo', bar: { baz: true } }) 132 | // ^? const x0: { foo: 'foo'; bar: { baz: true; }; } 133 | const x1 = foo([1, 2, true]) 134 | // ^? const x1: [1, 2, true] 135 | ``` 136 | 137 | ## 总结 138 | 139 | 看到这里我们便对 Narrow 类型构造器的机制与实现原理有了一定的了解了,我们可以简单总结一下: 140 | * 通过一个 generic 类型来诱导 TypeScript 的隐式推断 141 | * 通过在 literal object 的 ValueType 上进行类型运算来诱导 TypeScript 的类型缩小 142 | * 通过在 empty tuple 上进行类型运算来诱导 TypeScript 的类型缩小 143 | 144 | > 拓展阅读: 145 | > * [ts-toolbelt - Function Narrow](https://github.com/millsp/ts-toolbelt/blob/319e55123b9571d49f34eca3e5926e41ca73e0f3/sources/Function/Narrow.ts#L32) 146 | > * [Suggestion: Const contexts for generic type inference](https://github.com/microsoft/TypeScript/issues/30680) 147 | > * [Excess Property Checks](https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks) 148 | -------------------------------------------------------------------------------- /zh-Hans/What type? - TypeScript boundary.md: -------------------------------------------------------------------------------- 1 | # 介于 TypeScript 与 JavaScript 之间 2 | 3 | 在这里我将 `as`, `is`, `satisfies` 和 `in` 也归类于此节,实际上来说它们也算是确保一个什么类型的行为,只是位于 TypeScript 与 JavaScript 的边界中,将变量的类型显式的与目标的类型按照规则进行匹配。 4 | 5 | ## 赞美 `as` 6 | 7 | 对于 `as` 这种常见的操作符来说,一般会在几个位置中出现。 8 | * 作为 JavaScript 与 TypeScript 的交界处,将某一个 JavaScript 中的值作为一个类型去与 TypeScript 中的类型进行断言 9 | * 作为 TypeScript 中的类型断言,将某一个 TypeScript 中的类型作为一个类型去与 TypeScript 中的类型进行断言 10 | 11 | 在这里我采用了[《TypeScript Deep Dive》](https://basarat.gitbook.io/typescript/type-system/type-assertion#type-assertion-vs.-casting)中的说法,倾向于 `as` 是作为一个「Assertion(断言)」,而不是一个「Casting(转化)」。 12 | 13 | ```typescript 14 | interface A { 15 | a: string 16 | } 17 | // 作为 JavaScript 与 TypeScript 的交界处 18 | const a0 = { a: '1' } as A 19 | const a1 = { b: 1 } as A // TS2352: Conversion of type '{ b: number; }' to type 'A' may be a mistake 20 | // because neither type sufficiently overlaps with the other. 21 | // If this was intentional, convert the expression to 'unknown' first. 22 | // Property 'a' is missing in type '{ b: number; }' but required in type 'A'. 23 | // 在上面我们断言了 `{ b: 1 }` 是一个 A 类型,但是实际上他完全和 A 没有关系 24 | // 于是乎 TypeScript 便也给我们抛出来了一个错误 25 | const a2 = { } as A 26 | const a3 = { a: '1', b: 1 } as A 27 | 28 | type A0 = { a: string } extends A ? true : false 29 | // ^? type A0 = true 30 | type A1 = { b: number } extends A ? true : false 31 | // ^? type A1 = false 32 | // 在上面的 `{}` 实际上被**隐式推断**为了 `any`,所以这里使用 `any` 进行测试 33 | // 这里我们暂且记住**隐式推断**,在下文中我会去解释他 34 | type A2 = [any] extends [A] ? true : false 35 | // ^? type A2 = true 36 | type A3 = { a: string, b: number } extends A ? true : false 37 | // ^? type A3 = true 38 | ``` 39 | 40 | 上面是我们对 `as` 的一个简单的了解,他的行为很类似于使用 `extends` 对俩个类型进行推断。但是我们也从中发现了一些令人感到十分疑惑不解的部分,为什么我们使用 `{}` 会被**隐式推断**为 `any` 呢?他会不会在其他的地方被触发呢? 41 | 在我们对 `as` 进行深入探索之前我们需要回忆一下在 TypeScript Handbook 中对 `as` 行为的这句定义「[转化向一个更具体(more specific)或者更不具体(less specific)版本的类型](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#:~:text=convert%20to%20a%20more%20specific%20or%20less%20specific%20version%20of%20a%20type)」。 42 | ```typescript 43 | type A = 1 44 | 45 | // 在这里当我们尝试将 1 断言为 A 时,表面上看一切都是没问题的 46 | const a0 = 1 as A 47 | // 但是实际上我们在触发断言操作的前方出现了一个 literal primitive type 48 | // 实际上他会触发一个隐式推断,所以实际的执行效果是 `2 as number as A` 49 | const a1 = 2 as A 50 | const a2 = Number() as A 51 | ``` 52 | 53 | 对于上面的几个来说类型的运算看起来都能通过 `V as number as A` 进行解释,但是在下面可以发现了一个问题,哪怕我们强制 `as` 了一个 `const` 的类型,又或者是依赖 TypeScript 自己的 literal type 转化出来的 const 类型也能通过类型校验。 54 | ```typescript 55 | const t0 = 2 56 | const t1 = t0 as A 57 | const t2 = 2 as const as A 58 | const t3 = 2 as 2 as 1 59 | ``` 60 | `2 as 1` 这是一个被 TypeScript 被允许的行为,但是只被允许在 primitive type 中,当俩个 literal primitive type 的 `supertype` 一致时,则会被允许被断言所操作。 61 | ```typescript 62 | const x0 = 1 as true // TS2352: Conversion of type 'number' to type 'true' may be a mistake because neither type sufficiently overlaps with the other. 63 | // If this was intentional, convert the expression to 'unknown' first. 64 | const x1 = true as boolean 65 | // less specific 66 | const x2 = Boolean() as true 67 | // more specific 68 | 69 | type B = { b: 1 } 70 | 71 | const b0 = { b: 1 } as B 72 | const b1 = { b: 2 } as B // TS2352: Conversion of type '{ b: 2; }' to type 'B' may be a mistake because neither type sufficiently overlaps with the other. 73 | // If this was intentional, convert the expression to 'unknown' first. 74 | // Types of property 'b' are incompatible. 75 | // Type '2' is not comparable to type '1'. 76 | const b2 = { b: 2 as number } as B 77 | const b3 = { b: 2 } 78 | const b4 = b3 as B 79 | const b5 = { b: 2 } as const 80 | const b6 = b5 as B // TS2352: Conversion of type '{ readonly b: 2; }' to type 'B' may be a mistake because neither type sufficiently overlaps with the other. 81 | // If this was intentional, convert the expression to 'unknown' first. 82 | // Types of property 'b' are incompatible. 83 | // Type '2' is not comparable to type '1'. 84 | type C = [1] 85 | 86 | const c0 = [1] as C 87 | const c1 = [2] as C // TS2352: Conversion of type '[2]' to type 'C' may be a mistake because neither type sufficiently overlaps with the other. 88 | // If this was intentional, convert the expression to 'unknown' first. 89 | // Type '2' is not comparable to type '1'. 90 | const c2 = [2 as number] as C 91 | const c3 = [2] 92 | const c4 = c3 as C 93 | const c5 = [2] as const 94 | const c6 = c5 as C // TS2352: Conversion of type 'readonly [2]' to type 'C' may be a mistake because neither type sufficiently overlaps with the other. 95 | // If this was intentional, convert the expression to 'unknown' first. 96 | // The type 'readonly [2]' is 'readonly' and cannot be assigned to the mutable type 'C'. 97 | ``` 98 | 在上面关于 B 和 C 类型的测试中,我们可以看到一旦 literal primitive type 进入到 `{}`、`[]` 中便会丢失该特性,那么我们再来尝试将其拆出来进行验证。 99 | 100 | ```typescript 101 | const b00 = 2 as B['b'] 102 | const c00 = 2 as C[number] 103 | const c01 = 2 as C[0] 104 | ``` 105 | 106 | 好了,基于上述我们对 `as` 的各种行为观察,我们可以得到一些结果。 107 | * 当 `as` 操作的左侧类型出现了 literal primitive type 时 TypeScript 会将其隐式推断为其的 `supertype`,如:`1` -> `number`、`true` -> `boolean`,在这个基础上进行是否为 supertype 或 subtype 的判断。 108 | * 当 `as` 操作的左侧为非 primitive 的 literal value 时,将其隐式推断为对应的 const 类型,再进行是否为 supertype 或 subtype。 109 | * 当 `as` 操作的左侧为 `{}` 类型时,隐式转化为 any 进行是否为 supertype 或 subtype 的判断。 110 | 111 | 我们再以一图解释一下 `as any as T` 是如何工作的: 112 | 113 | as any as T 117 | 118 | 在 top type 与 bottom type 之间的任意类型,都可以通过 more specific 和 less specific 来到 top 或者 bottom 中,再从其前往任意一个类型,从而解决了不同类型之间的隔断。 119 | 120 | 所以其实除了 `as any as T` 还有 `as unknown`、`as never` 均可用于解决该问题。 121 | 122 | > 拓展阅读: 123 | > * [Type hierarchy tree](https://www.zhenghao.io/posts/type-hierarchy-tree) 124 | > * [Unknown vs any](https://stackoverflow.com/a/67314534/15375383) 125 | > 126 | > 相比较使用 `any` 作为中间类型,使用 `unknown` 会更好,因为前者是编译器开的洞,至于为什么不是 `never` 呢,主要还是语义看起来有点奇怪。 127 | 128 | ### `as` 与字面量 129 | 130 | 除了上面聊到的 `as` 一个具体的类型,我们还可以使用 [ts@3.4 - const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) 去将一个值转化为其对应的 literal type,主要是为了解决 `const` 的类型推断问题。 131 | 132 | 同时与 const 相关的还有一些奇妙的东西,如果我们想这么做: 133 | ```typescript 134 | declare function foo(t: T): T[0] 135 | const t0 = foo([2]) 136 | // ^? const t: number 137 | // 如果我们需要一个被 `[]`、`{}` 包裹后的字面量的 const 类型,我们只能要求用户主动声明 const 138 | const t1 = foo([2] as const) 139 | // ^? const t: 2 140 | ``` 141 | 但是这个很明显对 JavaScript 用户或者特定需求下的调用方显得不是那么友好,如果某个函数一定需要一个 const 的类型去进去运算的时候我们还一定要用户手动进行 `as const` 显然是不太合适的,当然我们的 TypeScript 也考虑到了该种情况的需求,新增了[ts@5.0 - `const` 142 | Type Parameters](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters)特性。不过相对来说 5.0 对于目前编写时来说还算是一个相对比较新的版本,还未被普遍采用,那么我们有没有[别的办法](./What%20type%3F%20-%20TypeScript%20boundary%20-%20Narrow%20type.md)来实现我们的需求呢? 143 | 144 | > 拓展阅读: 145 | > * [Const contexts for literal expressions](https://github.com/microsoft/TypeScript/pull/29510) 146 | > * [`const` modifier on type parameters](https://github.com/microsoft/TypeScript/pull/51865) 147 | 148 | ## 不简单的 `satisfies` 149 | 150 | 与 `as` 的「在 JavaScript 与 TypeScript 的交界处,将某一个 JavaScript 中的值作为一个类型去与 TypeScript 中的类型进行断言」中的作用类似,不过[稍稍的有点不同](https://github.com/microsoft/TypeScript/issues/47920#:~:text=Desired%20Behavior%20Rundown)。他作为一个在 TypeScript@4.9 中被引入的[新特性](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator),主要是为了解决一些特定的问题,在这里我们不过多对 Handbook 中的内容进行过多的赘述。 151 | 152 | ### 一个久远的 issue 153 | 154 | 在古早时期的 TypeScript 的 issue 中有一个关于 [`Exact Types` 的讨论](https://github.com/microsoft/TypeScript/issues/12936),目前关于这个讨论的部分已经被 `satisfies` 所解决,但是仍然还存有一些需要解决的问题。 155 | 156 | 首先当我们需要值的类型是被受限的,而不是尽可能宽泛的时候,我们可以使用 `satisfies` 来进行类型的约束,举例子来说: 157 | ```typescript 158 | interface A { 159 | a: string 160 | } 161 | 162 | const a = { 163 | a: '132', 164 | b: 1 165 | //^ TS1360: Type '{ a: string; b: number; }' does not satisfy the expected type 'A'. 166 | // Object literal may only specify known properties, and 'b' does not exist in type 'A'. 167 | } satisfies A 168 | // 我们通过指定我们需要一个 `satisfies` 的 `A` 169 | // 那么编译器便帮我们检查了这个值的类型是否出现了不符合 `A` 的约束 170 | // 在这里是需要满足 `A` 的一个 subtype 或者是 `A` 本身 171 | ``` 172 | 但是如果我们通过范型的形式呢? 173 | ```typescript 174 | interface A { 175 | a: string 176 | } 177 | 178 | declare function foo(t: T): T 179 | 180 | const a = foo({ 181 | a: '132', 182 | b: 1 183 | }) 184 | ``` 185 | 在这里我们可以传递一个 `A` 的 supertype ,但是如果我们不希望被传递一个 supertype 而是一个 subtype 呢?直观的来说,就是我们不想被传递 `b` 这个属性。 186 | -------------------------------------------------------------------------------- /zh-Hans/What type?.md: -------------------------------------------------------------------------------- 1 | # 什么“变量”? 2 | 3 | 你以为我要教你类型“变量”有哪几种?教你有哪些基本类型?那就又大错特错了! 4 | 5 | 在实际的类型运算中,我们比较常用的一个功能便是比较一个类型是否和另外一个类型逆变、协变、不变关系。通过用这些关系的是否满足,从而来触发一些我们需要的运算与校验。接下来我会介绍几种常用的方式: 6 | 7 | 在 TypeScript 中最基本的判断单元便是 `extends`,实际应用中他有许多的语义,而在这里仅需要一个将俩个类型进行判断的功能,所以我会避开那些造成歧义的用法,而单独介绍如何判断俩个类型的关系。 8 | 9 | ## 一点小小的 never 震撼 10 | 11 | 常见的我们会去判断一个类型是否为 Primitive 类型,这也是最常见的需求 12 | ```typescript 13 | type IsString = X extends string ? true : false 14 | 15 | type T0 = IsString<'a'> 16 | // ^? type T0 = true 17 | type T1 = IsString 18 | // ^? type T1 = true 19 | type T2 = IsString 20 | // ^? type T2 = false 21 | ``` 22 | 23 | 另外的,时常会有一些特殊情况我们需要去处理,比如这个类型 [`never`](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type)。在 TypeScript 中,这个类型意味着一个不存在值的类型,从集合的角度来看,便是[空集](https://zh.wikipedia.org/wiki/%E7%A9%BA%E9%9B%86)、[单位元(幺元)](https://zh.wikipedia.org/zh-cn/%E5%96%AE%E4%BD%8D%E5%85%83)。 24 | 基本知识我们了解到了,那我们来思考一下一个问题,我如果通过 `extends` 运算去判断一个类型是不是 `never` 会发生什么呢? 25 | ```typescript 26 | type T0 = never extends never ? true : false 27 | // ^? type T0 = true 28 | // 很好,这里是符合预期的 29 | type IsNever = T extends never ? true : false 30 | 31 | type T1 = IsNever 32 | // ^? type T1 = never 33 | // 但是当我们在泛型中使用他的时候却发现变成了 never 34 | ``` 35 | 36 | 在这里我们需要知道一个关于 `extends` 的问题,实际上 ts 对于类型系统并没有引入过多的运算符,在这里便遇到了与之相关的一个问题,`extends` 他并不只有判断的语义。他同时还存在一个很特殊的情况,他会尝试拆解右值(在 `extends` 右侧的类型)如果为 union type 则按照规则遍历运行得到一个新的 union type。 37 | 38 | > 具体关于拆解遍历行为,请参考[ts@2.8 - Distributive conditional types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types) 39 | 40 | 而我们在这里使用 `never` 便是一个特殊的 union type,它有没有任何一个成员,所以遍历它最后便只能得到一个“新的” never 回来,所以我们在这里看起来是无法直接处理这个问题。 41 | 42 | 但是,怎么可能呢?我们将思路打开,如果是个空我们就无法得到我们的预期,那么我们便可以构造一个类型,在外面包裹上一个新的类型来防止 TypeScript 的遍历 never 行为,可以是 `{}` 也可以是 `[]`(这里我们暂时不提函数的包裹方式)。简单写个例子: 43 | ```typescript 44 | type IsNever = [T] extends [never] ? true : false 45 | type T0 = IsNever 46 | // ^? type T0 = true 47 | ``` 48 | 这样我们便能知道一个“变量”到底是不是一个 `never` 了。 49 | 50 | > 思考一下:如何写一个 IsBoolean 类型运算? 51 | 52 | ## 奇妙的函数类型 53 | 54 | 在上面我们主要讨论的了作为非函数类型他们之间“是不是”的一个判断方式行为以及特殊情况,接下来我们便要去了解更深入一点的关于函数的判断规则。 55 | 56 | 函数作为 JavaScript 中的一等公民,在类型系统中也有举足轻重的地位,函数之间的关系判断使用的也是 `extends` 但是在这里存在一些问题我们需要注意的,在一般情况(不涉及泛型与函数重载)下满足[类型构造符→对输入类型是逆变的而对输出类型是协变的。](https://zh.wikipedia.org/wiki/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98#:~:text=%E7%B1%BB%E5%9E%8B%E6%9E%84%E9%80%A0%E7%AC%A6%E2%86%92%E5%AF%B9%E8%BE%93%E5%85%A5%E7%B1%BB%E5%9E%8B%E6%98%AF%E9%80%86%E5%8F%98%E7%9A%84%E8%80%8C%E5%AF%B9%E8%BE%93%E5%87%BA%E7%B1%BB%E5%9E%8B%E6%98%AF%E5%8D%8F%E5%8F%98%E7%9A%84%E3%80%82) 57 | 58 | 简单的介绍完了,接下来我们利用一下「输入类型是逆变的而对输出类型是协变的」这一点做一些有趣的事情,在这里我假设一个需求:当俩个函数类型为何种关系的时候能对应的函数类型能被另一个函数类型所替换。 59 | ```typescript 60 | interface A { 61 | a: string 62 | } 63 | interface B { 64 | a: string 65 | b: number 66 | } 67 | declare function fxo(): A 68 | declare function fyo(): B 69 | 70 | let a = fxo() 71 | a = fyo() 72 | // fyo extends fxo 73 | // fxo 能被替换为 fyo 74 | type A0 = typeof fyo extends typeof fxo ? true : false 75 | // ^? type A0 = true 76 | 77 | let b = fyo() 78 | b = fxo() // Property 'b' is missing in type 'A' but required in type 'B'.ts(2741) 79 | // fxo not extends fyo 80 | // fyo 不能被替换为 fxo 81 | type B0 = typeof fxo extends typeof fyo ? true : false 82 | // ^? type B0 = false 83 | 84 | // 总结的来说便是,当输出类型为协变时,即被替换目标(fxo)的输出类型**小于等于**替换目标(fyo)时才能使得前者能替换为后者 85 | 86 | declare function fmo(a: A): void 87 | declare function fno(a: B): void 88 | 89 | fmo({ a: 'a' }) 90 | fno({ a: 'a' }) // Argument of type '{ a: string; }' is not assignable to parameter of type 'B'. 91 | // Property 'b' is missing in type '{ a: string; }' but required in type 'B'.ts(2345) 92 | // fno not extends fmo 93 | // fmo 不能被替换为 fno 94 | type A1 = typeof fno extends typeof fmo ? true : false 95 | // ^? type A1 = false 96 | 97 | // 这里的 as 是为了对齐 98 | fno({ a: 'a', b: 1 } as B) 99 | // 这里的 as 是为了防止 TypeScript 的 literal type 优化 100 | fmo({ a: 'a', b: 1 } as B) 101 | // fmo extends fno 102 | // fno 能被替换为 fmo 103 | type B1 = typeof fmo extends typeof fno ? true : false 104 | // ^? type B1 = true 105 | 106 | // 总结的来说便是,当输入类型为逆变时,即被替换目标(fno)的输出类型**大于等于**替换目标(fmo)时才能使得前者能替换为后者 107 | ``` 108 | 109 | 好了,到这里我们便对函数的逆变协变有了一定的了解,我们来整点特殊的类型来看看应该怎么做。 110 | ```typescript 111 | interface A { 112 | a: string 113 | } 114 | interface B { 115 | a: '1' 116 | } 117 | declare function f0(g: G): G extends A ? 1 : 2 118 | declare function f1(g: G): G extends B ? 1 : 2 119 | 120 | let t0 = f0({ a: '' }) 121 | // ^? let t0: 1 122 | t0 = f1({ a: '' }) // Type '2' is not assignable to type '1'.ts(2322) 123 | ``` 124 | 我们回忆下在上面的总结「当输出类型为协变时,即被替换目标的输出类型**小于或等于**替换目标时才能使得前者能替换为后者」。 125 | ```markdown 126 | 在这里假设我们需要将 f0 替换为 f1 127 | => 我们就需要让 f0 的输出类型**小于或等于** f1 的输出类型 128 | => f0 的输出类型由 G 与 A 共同决定,f1 也是如此 129 | => 俩个函数的 G 都会由用户的输入而决定,无法被保证 130 | => 那么这个时候如果需要能够替换,那么就需要 A 的类型**小于或等于** B 的类型 131 | => 而该输出类型还受 G 的影响,所以我们需要 G extends A 与 G extends B 都成立 132 | => 只有当 `A === B` 时,俩个函数的输出类型才能满足协变关系 133 | => 所以我们可以得知 `F extends F` 时,`A === B` 134 | ``` 135 | 136 | 那么我们知道了这个有什么用呢?比如说 any 作为 top type ,使用很多的办法都没有办法判断一个类型是不是 any,但是我们通过这个就能判断你的同事是不是传了个 any 进来了!是不是很有用,那么接下来我们来写一段代码看看: 137 | ```typescript 138 | type IsEqual = ( 139 | () => G extends A ? 1 : 2 140 | ) extends ( 141 | () => G extends B ? 1 : 2 142 | ) ? true : false 143 | 144 | type T0 = IsEqual 145 | // ^? type T0 = false 146 | type T1 = IsEqual 147 | // ^? type T1 = true 148 | type T2 = IsEqual 152 | type T3 = IsEqual<{ 153 | // ^? type T3 = false 154 | a: '1' 155 | }, A> 156 | type T4 = IsEqual 157 | // ^? type T4 = false 158 | ``` 159 | 160 | > 拓展阅读: 161 | > * [介于 TypeScript 与 JavaScript 之间](./What%20type%3F%20-%20TypeScript%20boundary.md) 162 | --------------------------------------------------------------------------------