TypeScript

Distributive conditional types

首先这个实现并不是很合理,很反直觉,定义如下

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

由于 裸类型 在 conditional type 操作中的分裂现象,会导致一个 裸的 union,和一个操作符获取的 union 在进行 conditional 操作时会得到不同的接口。

执行 Extract,实际情况经过泛型参数的传递 keyof R2 会被先行处理为一个 union,此时就变为了 ‘a’ | ‘b’ | ‘c’,是一个 naked type parameter 对其进行 extends操作,被转化为 (‘a’ extends ‘a’) | (‘b’ extends ‘a’) | (‘c’ extends ‘a’),结果为 ‘a’
而直接执行 keyof R2 extends ‘a’ ? keyof R2 : never,keyof R2 未被先转化为 union,此刻便不是一个 naked type parameter,也就是说 keyof 会生成一个 wrap, 使得此类型不是一个 裸 union,而泛型参数传递的过程,会导致这层 wrap 被去除

https://stackoverflow.com/questions/51651499/typescript-what-is-a-naked-type-parameter When they say naked here, they mean that the type parameter is present without being wrapped in another type, (ie, an array, or a tuple, or a function, or a promise or any other generic type) Ex:

  1. type NakedUsage<T> = T extends boolean ? "YES" : "NO"
  2. type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO"; // wrapped in a tuple

The reason naked vs non nakes is important is that naked usages distribute over a union, meaning the conditional type is applied for each member of the union and the result will be the union of all application

  1. type Distributed = NakedUsage<number | boolean > // = NakedUsage<number> | NakedUsage<boolean> = "NO" | "YES"
  2. type NotDistributed = WrappedUsage<number | boolean > // "NO"
  3. type NotDistributed2 = WrappedUsage<boolean > // "YES"

Read here about conditional type distribution.

实际提示也有体现:
keyof 不是简单提示一个 union
image.png

  1. type NoDistribute<T> = [T] extends [T] ? T : never;
  2. type NoDistributeN<T> = T extends T ? T : never;
  3. type a4 = NoDistribute<keyof R2>;
  4. type a5 = NoDistributeN<keyof R2>;

通过将参数类型 T,始终放置在一个元组中,会避免这种拆 wrap 现象,而直接使用,则会拆 wrap:
image.png
image.png

有违直觉的例子:

  1. type R2 = {
  2. a: number;
  3. b: number;
  4. c: number;
  5. };
  6. type a2 = Extract<keyof R2, 'a'>; // 'a'
  7. type a3 = keyof R2 extends 'a' ? keyof R2 : never; // never
  8. type Omit<T, K extends Extract<keyof T, string>> = K extends any ? any : K;
  9. type WithoutFoo = Omit<{ foo: string }, "foo">; // ok
  10. type WithoutFooGeneric<P extends { foo: string }> = Omit<P, "foo">; // Error: Type '"foo"' does not satisfy the constraint 'Extract<keyof P, string>'.
  11. type NoDistribute<T> = [T] extends [T] ? T : never;

image.png
https://github.com/microsoft/TypeScript/issues/24560
关于泛型 + conditional type + index 的相关内容有挺多违反直觉的实现,值得关注。

以及讨论是否需要关闭这个功能:
https://github.com/microsoft/TypeScript/issues/29368
https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532
为了避免 裸类型 造成的上述现象:
可以对所有需要进行 参数传递+conditional判断 的 类型参数,都做元组包裹

  1. type RouteWithParamsMaker<T extends Route> = (keyof T['Params']) extends infer U
  2. ? [U] extends [string]
  3. ? RouteWithParams<U>
  4. : never
  5. : never;

FP

Fold

定义请求的中间状态
HTTP requests have one of four states

  • we haven’t asked yet
  • we’ve asked, but we haven’t got a response yet
  • we got a response, but it was an error
  • we got a response, and it was the data we wanted

With Flow we can easily define a type that represents these four states

  1. type RemoteData<E, D>
  2. = { type: 'NotAsked' }
  3. | { type: 'Loading' }
  4. | { type: 'Failure', error: E }
  5. | { type: 'Success', data: D };type Model = {
  6. things: RemoteData<HttpError, Array<Thing>>
  7. };

The nice thing about this data model is, the type checker will now force you to write the correct UI code. It will keep track of the possibility of “things not loaded” and errors, and force you to handle them all in the UI.

通过这样的类型定义,会强制使用该数据的开发者对错误的,loading的情况进行检测

Or, using a more functional style, let’s define a fold function that can be re-utilised in more use cases

或者使用一个 case switch,通常这类方法被称 fold

  1. type Maybe<A> = ?A;
  2. type Model = {
  3. things: Maybe<Array<Thing>>
  4. };
  5. function fold<R>(
  6. notAsked: () => R,
  7. loading: () => R,
  8. failure: (error: HttpError) => R,
  9. success: (data: Array<Thing>) => R
  10. ): (model: Model) => R {
  11. return ({ things }) => {
  12. return things.type === 'NotAsked' ? notAsked() :
  13. things.type === 'Loading' ? loading() :
  14. things.type === 'Failure' ? failure(things.error) :
  15. success(things.data) }
  16. }
  17. // 生成 fold 策略 再 获取数据
  18. const SomeView = fold(
  19. () => <div>Please press the button to load the things</div>,
  20. () => <div>Loading things...</div>,
  21. (error) => <div>An error has occurred { error }</div>,
  22. (data) => <div>{ data.map(thing => {}) }</div>
  23. )(model)

业务

分页可搜索选择最大选限制

背景

  1. 接口获取分页数据,可搜索,返回 { result: data[], total: number }
  2. 每页可选数据条为接口返回数据条,数据条数会因为搜索而变少,但真实数据条总数不变。
  3. 可设置 选择条数上限
  4. 提供全选按钮,选择数达到上限为满选,未选为空,有选择为半选;
    • 半选/空选 状态点击: 执行尽量满选,即当前页至上而下进行选择,直到选完当前页,或到达选择条数上限。
    • 全选 状态点击:执行 清空选择
  5. 选择组件提供onCheck接口,返回当前 全选,半选 内容

    注意点

  6. 由于分页数据来自后端,在普通分页请求中,total数据表示 真实数据条数;而此时如果做了搜索操作,total则不再表示真实数据条数,而是 符合条件数据数,不可设为 total。

  7. 如 真实数据条数 不足最大选限制,应视为特殊情况,也就是永不需要进行最大选限制判断。此时的最大选限制 = 真实数据条数
  8. 执行尽量满选时,选完 现有可选数据条数,或达到最大限制 停止。
  9. 由于是否满选为外部控制条件,check相关状态需采用受控模式。

可选择数据条数 = 请求返回数据条数
total = 真实数据条数
实际最大选限制 = 真实数据条数 < 项目设置最大选限制 ?真实数据条数 : 项目设置最大选限制
全选状态true = 已选数据条数 === 最大选限制

状态分配

固定常量:

  1. 项目设置最大选限制

状态变量:

  1. 可选数据/当页数据(请求返回数据条)selectableTables
  2. 真实数据条数(除搜索外返回total)selectableTablesOriginTotal
  3. 已选数据条数 actualChecked
  4. 实际最大选限制(真实数据条数,项目设置最大选限制)maxCanSelectedNum
  5. 受控选中状态(已选数据条数,真实数据条数,实际最大选限制) checkedControl
  6. 选择按钮状态(已选数据条数,最大选限制)
    1. allChecked: checkedKeys.find((i) => i === ‘listAll’)
    2. halfChecked: halfCheckedKeys.find((i) => i === ‘listAll’)
    3. empty: checkedKeys.filter((i) => i === ‘listAll’).length === 0

      分支分配

      用于计算并设置 已选数据条数
      1. if (
      2. /* 表可选数 永用不超过 最大选限制 */selectableTablesOriginTotal < maxSelectedTablesNum
      3. ) {
      4. ...
      5. } else if (
      6. /* 已经达到最大可选择数 */ actualChecked.length === maxCanSelectedNum
      7. ){
      8. ...
      9. } else if (
      10. /* 未达到最大选择数,需要执行选到最大数 */ checkedKeys.find((i) => i === 'listAll')
      11. ) else {
      12. /* 普通选中与反选 */
      13. }