TypeScript 允许推断类型参数的联合关键字

hfyxw5xn  于 6个月前  发布在  TypeScript
关注(0)|答案(7)|浏览(44)

建议

🔍 搜索词

  • 拓宽
  • 泛型
  • 开放

✅ 可实现性检查清单

我的建议符合以下准则:

  • 这不会对现有的TypeScript/JavaScript代码造成破坏性的更改
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同的JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、JS的新语法糖等)
  • 这个特性将与TypeScript's Design Goals的其他部分一致。

⭐ 建议

目前,TypeScript在决定何时拓宽泛型类型时有一些规则,这可能会导致相对相似的函数签名产生令人困惑的结果:

declare function fn1<T>(items: Array<T>): T
declare function fn2<T>(items: Array<() => T>): T
declare function fn3<T>(items: Array<{ prop: T }>): T

let res1: number | string = fn1([42, "hi"])
let res2: number | string = fn2([() => 42, () => "hi"]) // string ("hi") is not a number
let res3: number | string = fn3([{ prop: 42 }, { prop: "hi" }]) // string ("hi") is not a number

目前,它完全取决于TypeScript在未提供类型参数时的决定,以及TypeScript多年来已经发生了几次变化。
如果TypeScript能给你更多关于这种行为的控制权,以指定你的泛型是否拓宽,那将会很有帮助。
请忽略语法,只是演示我希望它会出现在哪里

// widen
declare function fn<widen T>(items: Array<() => T>): T;
fn([() => 1, () => 2, () => "three"]) // => number | string

// do not widen
declare function fn<donotwiden T>(items: Array<T>): T;
fn([1, 2, "three"]) // ERR

📃 激励示例

使用TypeScript生成类型的越来越流行的方式是,已经有了许多专门用于从运行时值推断精确类型的TypeScript功能,甚至有像zod和io-ts这样的库可以执行类似的事情:

let type = union(string(), number())

let value: unknown = ...
assert(value, type)
value // >> string | number

用TypeScript编写的库越来越多地依赖于TypeScript的值到类型的推断作为其公共API的一部分。因此,当TypeScript改变关于何时拓宽与否的规则时,这可能会导致更剧烈的破坏性更改,有时需要重新设计这些库。

type Check<T> = (value: unknown) => value is T

function string(): Check<string> {...}
function number(): Check<number> {...}
function union<T>(...members: Assertion<T>[]): Check<T> {...}

let assert = union(string(), number()) // ERR: number is not a string
let assert = union<string | number>(string(), number())

现在,你可以通过一些技巧来欺骗TypeScript获得期望的拓宽行为(通过谎报实际类型)。但是,不能保证这种行为会在TypeScript版本之间保持不变。

💻 用例

type Check<T> = (value: unknown) => value is T

function string(): Check<string> {...}
function number(): Check<number> {...}
function union<widen T>(...members: Check<T>[]): Check<T> {...}

let assert1 = union(string(), number())
let assert2 = union<string | number>(string(), number())
type SomeUnionToStayInSyncWith = string | number
let assert3 = union<SomeUnionToStayInSyncWith>(string(), number())
new9mtju

new9mtju1#

See Distributive Conditional Types
And this will make your examples work.
Example1

declare function fn1<T extends unknown[]>(items: T): T[number]
declare function fn2<T extends (() => unknown)[]>(items: T): ReturnType<T[number]>
declare function fn3<T extends {prop: unknown}[]>(items: T): T[number]['prop']

let res1: number | string = fn1([42, "hi"])
let res2: number | string = fn2([() => 42, () => "hi"]) // OK
let res3: number | string = fn3([{ prop: 42 }, { prop: "hi" }]) // OK

Example2

type Check<T> = (value: unknown) => value is T
type CheckedType<T extends Check<unknown>> = T extends Check<infer U> ? U : never;

function string(): Check<string> {
  return (value): value is string => typeof value === "string"
}

function number(): Check<number> {
  return (value): value is number => typeof value === "number"
}

function union<T extends Check<unknown>[]>(...members: T): Check<CheckedType<T[number]>> {
  return (value): value is CheckedType<T[number]> => {
    for (let member of members) {
      if (member(value)) {
        return true
      }
    }
    return false
  }
}

let check1 = union(string(), number())
let x = '???' as string | number | {};
if (check1(x)) {
  x // OK! x is string | number
}
6psbrbz9

6psbrbz92#

当然,但是你已经改变了union()的类型参数签名。关键是类型参数是公共API的一部分

mw3dktmi

mw3dktmi3#

关于 nowiden 的情况,我不确定如何推理这里描述的行为。这三个行中,很可能有一个错误或者没有错误,但我认为很难证明 noWiden(arr1) 应该是一个错误,而且当我们内联这个表达式时,代码中没有什么有意义的变化。

declare function noWiden<nowiden T>(arr: T[]): void;
const arr1 = [Math.random() > 0.5 ? 1 : "two"];
const arr2 = [1, "two"];
noWiden(arr1);
noWiden(arr2);
noWiden([1, "two"]);

这里关于推断候选集合过程有很多微妙之处——尽管 OP 中的例子看起来相似,但从类型系统的Angular 来看,推断算法看到的是非常不同的东西。我不认为这令人惊讶,但据我所知,我们实际上在过去并没有在这方面打破太多(欢迎针对我自己学习的反例)。

我认为这里的直觉是“除非其中一个推断候选项是联合体,否则不要推断联合体”。...但在这些情况下,实际上其中一个推断候选项就是联合体。必须有一些更强大的理论原则来依赖于这种方式,以便在某种程度上在局部重构参数的情况下保持一致性。

在我看来, widen 更加清晰。在候选集合之后有一个步骤,大致上说,如果我们不能通过制作联合体(在没有约束说明其他情况的情况下)产生一个统一的类型,那么就发出一个错误并回退到约束类型。这可以很容易地根据类型参数声明进行依赖,然后一切都会从那里开始工作。

一些更实际的使用案例将有助于帮助考虑。

fhity93d

fhity93d4#

我并没有真正使用nowiden的场景,我只是在假设这个过程是如何在内部工作的。如果这是不合理的,我很乐意放弃它。

在候选人收集之后有一个步骤,大致上说,如果我们不能通过制作联合(在没有约束说明其他情况的情况下)来产生一个统一的类型,那么就发出一个错误并回退到约束类型。这可以很容易地依赖于类型参数声明,从那里开始几乎所有事情都可以正常工作。

我想你可以为了讨论的目的将widen重命名为prefer-union:

declare function oneOf<prefer-union T>(a: T, b: T): T

它的操作方式如下:

oneOf("hello", "world") // string
oneOf("hello", 42) // string | number

实际上,TypeScript已经有了所需的prefer-union行为:

oneOf(["hello"], [42]) // string[] | number[]
oneOf({ a: "hello" }, [42]) // { a: string } | number[]

然后,TypeScript在某些地方产生了联合,但可能不适合prefer-union:

// what it is today (without prefer-union):
oneOf({ a: "hello" }, { b: 42 }) // { a: string, b?: undefined } | { a?: undefined, b: number }
// should probably be:
oneOf({ a: "hello" }, { b: 42 }) // { a: string } | { b: number }

最后,TypeScript放弃了产生联合,而是选择了其他方式:

// what it is today (without prefer-union):
oneOf(() => "hello", () => 42) // Type 'number' (42) is not assignable to type 'string'
// should probably be:
oneOf(() => "hello", () => 42) // (() => string) | (() => number)
vwhgwdsa

vwhgwdsa5#

以下是Markdown格式的翻译结果:

这是我们用于推断联合类型的用例。我没能解决在调用我们的isOneOf类型守卫时,需要手动声明联合类型的问题。一个新的关键字似乎是一个优雅的解决方法,所以给你我的支持。

7qhs6swi

7qhs6swi6#

Question: Which place makes more sense for TypeScript internally?

function oneOf<widen T>(...guards: Array<(value: unknown) => value is T): value is T
//            ^^^^^ declaration
function oneOf<T>(...guards: Array<(value: unknown) => value is widen T): value is T
//                                                              ^^^^^ reference
ycggw6v2

ycggw6v27#

如果将oneOf的定义更改为

declare function oneOf<T,U>(a: T, b: U): T | U

,它不会对

const f = oneOf(() => "hello", () => 42) // (() => string) | (() => number)  NO ERROR

产生错误。如果你确实想约束ab具有相同的类型(但你并不想),那么你需要其他的东西,所以当前declare function oneOf<T,U>(a: T, b: T): T的行为似乎没有用例。
至于这个oneOf的例子,也许问题应该是,

declare function oneOf<T,U>(a: T, b: T): T

declare function oneOf<T,U>(a: T, b: T): T | U

是否打算表达相同的语义?(我认为是的)。
那么为什么它们表现不同呢?
前者短了4个字符。
或许可以称之为一个bug。
这里有另一个看似在语义上相同但遇到问题的泛型示例。

相关问题