Typescript:如何正确键入此条件数组函数?

时间:2019-05-15 01:16:43

标签: typescript

我想用条件元素定义一些数组,但对here概述的方法不满意,因此我创建了一个帮助器函数来使声明更整洁。辅助函数在原始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)

2 个答案:

答案 0 :(得分: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())
]);

类型基本上是正确的,但是我似乎无法让编译器从类型为T的值推断出Init<T>。我猜想嵌套/圆形类型实在太多了。因此,我不仅要调用conditionalArray(addIf => ...),还必须调用conditionalArray<string>(addIf => ...),否则T会得到{}的“默认”值,并且会同时出现错误和范围过大数组类型Array<{}>作为输出。

希望有帮助。


更新:良好的调用使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<1 | "a">是推断值[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,那么即使恰好只包含数字。

但是,再次,您可以通过条件类型强制编译器通过以下逻辑:

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")[]

那是因为在string的{​​{3}}中明确提到了numberbooleanT给编译器一个提示,即需要使用文字类型

有关编译器选择如何以及何时选择扩展或保留文字类型的更多详细信息,请参见generic constraint


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

答案 1 :(得分:0)

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")
])