这是我目前能做的最好的:
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<T> 类型的值推断T。我猜嵌套/圆形类型对它来说太多了。 ? 因此,我必须调用conditionalArray<string>(addIf => ...),而不是仅仅调用conditionalArray(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,那么即使 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 中明确提及 string、number 和 boolean 会提示编译器需要文字类型。
有关编译器如何以及何时选择扩展或保留文字类型的更多详细信息,请参阅Microsoft/TypeScript#10676。
好的,希望对您有所帮助。祝你好运!