【问题标题】:Typescript: how to properly type this conditional array function?Typescript:如何正确键入此条件数组函数?
【发布时间】:2019-05-15 01:16:43
【问题描述】:

我想定义一些带有条件元素的数组,但对here 概述的方法不满意,所以我创建了一个辅助函数来使声明更清晰。辅助函数在 vanilla JavaScript 中很简单,但由于泛型问题,我在键入它时遇到了麻烦。

JavaScript 版本

const nin = Symbol('nin')

const includeIf = (condition, item) =>
    (typeof condition === "function" ? condition(item) : condition) ? item : nin

const conditionalArray = (init) =>
    init(includeIf).filter(item => item !== nin)

/* USAGE */

const cond = false

// should equal ['foo', 'bar', 'qux'] and have type string[]
const arr1 = conditionalArray(addIf => [
    'foo',
    'bar',
    addIf(cond, 'baz'),
    addIf(word => word.length < 10, 'qux')
])

// should equal [{ name: 'Alice', age: 23 }] and have type { name: string, age: number }[]
const arr2 = conditionalArray(addIf => [
    { name: 'Alice', age: 23 },
    addIf(false, { name: 'Bob', age: 34 }),
    addIf(person => person.age > 18, { name: 'Charlie', age: 5 })
])

jcalz 的帮助下更新了 TypeScript 版本

type Narrowable = string | number | boolean | undefined | null | void | {};

const nin = Symbol('nin')

type AddIf = <T, U>(condition: ((x: T) => boolean) | boolean, itemIfTrue: T, itemIfFalse?: U | typeof nin) => T | U | typeof nin
const addIf: AddIf = (condition, itemIfTrue, itemIfFalse = nin) => {
    return (typeof condition === "function" ? condition(itemIfTrue) : condition) ? itemIfTrue : itemIfFalse
}

const conditionalArray = <T extends Narrowable>(init: (addIf: AddIf) => Array<T | typeof nin>) =>
    init(addIf).filter((item): item is T => item !== nin)

【问题讨论】:

    标签: typescript


    【解决方案1】:

    这是我目前能做的最好的:

    const nin = Symbol('nin')
    
    // T is the array element type    
    const includeIf = <T>(condition: boolean | ((x: T) => boolean), item: T) =>
        (typeof condition === "function" ? condition(item) : condition) ? item : nin
    
    // describe the shape of the callback
    type Init<T> = (cb:
        (condition: boolean | ((x: T) => boolean), item: T) => T | typeof nin
    ) => (T | typeof nin)[]
    
    // T is the element type of the array.  Accept an Init<T>, produce a T[]
    export const conditionalArray = <T>(init: Init<T>) =>
        init(includeIf).filter((item: T | typeof nin): item is T => item !== nin)
    
    
    const cond = true    
    declare function generateWord(): string
    
    // need to manually specify <string> below ?:
    const arr = conditionalArray<string>(addIf => [
        "foo",
        "bar",
        addIf(cond, "baz"),
        addIf(word => word.length < 10, generateWord())
    ]);
    

    类型基本上是正确的,但我似乎无法让编译器从Init&lt;T&gt; 类型的值推断T。我猜嵌套/圆形类型对它来说太多了。 ? 因此,我必须调用conditionalArray&lt;string&gt;(addIf =&gt; ...),而不是仅仅调用conditionalArray(addIf =&gt; ...),否则T 会得到{} 的“默认”值,你会得到错误和太宽的数组类型Array&lt;{}&gt; 作为输出.

    希望对你有所帮助。


    更新:良好的调用使init 的类型仅在其返回值的类型中通用;这似乎足以使编译器感到困惑,从而可以进行推理。

    所以这是我们目前最好的:

    const nin = Symbol('nin')
    
    type IncludeIf = typeof includeIf
    const includeIf = <T>(condition: ((x: T) => boolean) | boolean, item: T): T | typeof nin => {
        return (typeof condition === "function" ? condition(item) : condition) ? item : nin
    }
    
    const conditionalArray = <T>(init: (includeIf: IncludeIf) => Array<T | typeof nin>) =>
        init(includeIf).filter((item): item is T => item !== nin)
    

    解决您的问题:

    const arr1 = conditionalArray(addIf => [
        addIf(true, 1), addIf(true, 'a')
    ]); // Array<1 | "a"> 
    

    你确定这太严格了吗? There's a lot of machinery in TypeScript around trying to determine when literal types should be left narrow or widened。我认为Array&lt;1 | "a"&gt; 是推断[1, "a"] 值的完全合理的类型。如果你想扩大它,你可以告诉编译器 1'a' 不是字面意思:

    const arr1 = conditionalArray(addIf => [
      addIf(true, 1 as number), addIf(true, 'a' as string)
    ])
    

    如果你真的想强制 conditionalArray() 的返回类型总是被加宽,那么你可以使用这样的条件类型:

    type WidenLiterals<T> =
        T extends string ? string :
        T extends number ? number :
        T extends boolean ? boolean :
        T
    
    const conditionalArray = <T>(
      init: (includeIf: IncludeIf) => Array<T | typeof nin>) =>
      init(includeIf).filter((item): item is T => item !== nin) as
      Array<WidenLiterals<T>>;
    
    const arr1 = conditionalArray(addIf => [
      addIf(true, 1), addIf(true, 'a')
    ]) // Array<string | number>
    

    这行得通,但它可能比它的价值更复杂。


    您的下一期:

    const arr2 = conditionalArray((addIf) => [
      1, 2, 3, addIf(true, 4), addIf(false, '5'), addIf(false, { foo: true })
    ]); // Array<number | "5" | {foo: boolean}>
    

    当您将 literal false 作为addIf() 中的条件传递时,编译器识别对您来说有多重要?我希望在现实世界的代码中你永远不会这样做......如果你在编译时知道条件是false,那么你就会把它排除在数组之外。相反,如果在编译时不确定条件是true 还是false,那么即使 value 恰好只包含数字。

    不过,同样,您可以强制编译器通过条件类型执行此类逻辑:

    const includeIf = <T, B extends boolean>(condition: ((x: T) => B) | B, item: T) => {
      return ((typeof condition === "function" ? condition(item) : condition) ? item : nin) as
        (true extends B ? T : never) | typeof nin;
    }
    
    const arr2 = conditionalArray((addIf) => [
      1, 2, 3, addIf(true, 4), addIf(false, '5'), addIf(false, { foo: true })
    ]) // Array<number>
    

    这行得通,但同样,它可能比它的价值更复杂。


    更新 2:

    假设您想忘记文字 false 并且希望在所有情况下都尽可能地保持元素类型为 narrow,您可以执行以下操作:

    type Narrowable = string | number | boolean | undefined | null | void | {};
    
    const conditionalArray = <T extends Narrowable>(
        init: (includeIf: IncludeIf) => Array<T | typeof nin>
    ) => init(includeIf).filter((item): item is T => item !== nin)
    
    const arr1 = conditionalArray(addIf => [1, "a"]);
    // const arr1: (1 | "a")[]
    

    这是因为在 generic constraint for T 中明确提及 stringnumberboolean 会提示编译器需要文字类型。

    有关编译器如何以及何时选择扩展或保留文字类型的更多详细信息,请参阅Microsoft/TypeScript#10676


    好的,希望对您有所帮助。祝你好运!

    【讨论】:

    • 感谢您的回复。我刚刚在上面添加了我当前的最佳尝试。你所拥有的与我一直在玩的非常接近。我也将{}[] 作为一种类型,然后我切换了它,所以现在它太具体了。我想我可能需要一些 infer 魔法,但我对它很陌生
    • 已更新以解决您当前最佳尝试中遇到的问题(尽管老实说,我会按照您的方式保留它)。
    • 我认为你完全正确地传递了一个 literal false,没有好的用例。但是,似乎将条件类型添加到 includeIf 也会扩大返回类型。我唯一关心的是使用文字类型而不是更广泛的类型是 conditionalArray((addIf) =&gt; [1, 'a']) 的类型为 (string | number)[]conditionalArray((addIf) =&gt; [1, addIf(true, 'a')]) 的类型为 (number | "a")[] 我认为这些类型应该是一致的。你知道为什么添加条件类型也会扩大返回类型吗?
    • 除了扩大类型以使它们保持一致之外,您是否知道是否可以将conditionalArray((addIf) =&gt; [1, 'a']) 键入为(1 | "a")[] 而不使其成为只读?
    • 再次更新。我在答案中链接了GitHub pull request,这样您就可以了解编译器如何决定何时保留文字类型以及何时扩大它们。坦率地说,我不知道为什么条件 includeIf 会改变事情;可能您必须阅读编译器代码和/或使用调试器逐步完成它才能理解,或者询问从事编译器工作的人(即,不是我)。当它与我想要的相反时,我有一些技巧来扩大/缩小文字,我感到很幸运。
    【解决方案2】:

    TypeScript 中的可能解决方案(模块导出/导入已删除)

    const nin = Symbol('nin')
    
    type includeIfFunc = (condition: boolean | ((item: string) => boolean), item: string) => Symbol | string;
    type addIfFunc = (addIfFunc: includeIfFunc) => (Symbol | string)[];
    type conditionalArrayFunc = (init: addIfFunc) => (Symbol | string)[];
    
    const includeIf: includeIfFunc = (condition: boolean | ((item: string) => boolean), item: string): Symbol | string =>
        (typeof condition === "function" ? condition(item) : condition) ? item : nin
    
    const conditionalArray: conditionalArrayFunc = (init: addIfFunc) =>
        init(includeIf).filter(item => item !== nin)
    
    const cond = true
    
    const arr = conditionalArray(addIf => [
        "foo",
        "bar",
        addIf(cond, "baz"),
        addIf(word => word.length < 10, "generateWord")
    ])
    

    【讨论】:

    • 感谢您的回复。抱歉,如果我的问题不清楚,但我正在尝试让 conditionalArray 函数处理任何类型的数组,而不仅仅是字符串
    • 你想要包含混合类型变量的同类型数组吗?
    • 我希望正确键入单一类型的数组,并希望包含多种类型的数组具有联合类型。即conditionalArray(addIf =&gt; [addIf(true, 1), addIf(true, 'a')]) 应该有类型(number | string)[]
    猜你喜欢
    • 2021-07-28
    • 1970-01-01
    • 1970-01-01
    • 2021-06-15
    • 1970-01-01
    • 1970-01-01
    • 2019-09-02
    • 2021-01-03
    • 2021-11-17
    相关资源
    最近更新 更多