简体   繁体   English

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

[英]Typescript: how to properly type this conditional array function?

I wanted to define some arrays with conditional elements but wasn't satisfied with approaches outlined here , so I created a helper function to make the declaration cleaner. 我想用条件元素定义一些数组,但是对这里概述的方法不满意,所以我创建了一个帮助器函数来使声明更整洁。 The helper function is simple enough in vanilla JavaScript, but I've been having trouble typing it due to issues with generics. 辅助函数在原始JavaScript中非常简单,但是由于泛型问题,我一直很难键入它。

JavaScript version 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 })
])

Updated TypeScript Version with help from jcalz 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)

This is the best I can do for now: 这是我目前可以做的最好的事情:

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())
]);

The typings are essentially correct, but I can't seem to get the compiler to infer T from a value of type Init<T> . 类型基本上是正确的,但是我似乎无法让编译器从Init<T>类型的值推断T I'm guessing the nested/circular types are just too much for it. 我猜想嵌套/圆形类型实在太多了。 😔 So instead of just calling conditionalArray(addIf => ...) , I have to call conditionalArray<string>(addIf => ...) , or else T gets the "default" value of {} and you get both errors and a too-wide array type Array<{}> as output. 😔因此,我不仅要调用conditionalArray(addIf => ...) ,还必须调用conditionalArray<string>(addIf => ...) ,否则T会获得{}的“默认”值,并且会同时出现两个错误并将数组类型太宽的数组Array<{}>作为输出。

Hope that's of some help, anyway. 无论如何,希望对您有所帮助。


Update: good call making the type of init only generic in the type of its return value; 更新:良好的调用使init类型仅在其返回值的类型中通用; that seems to un-confuse the compiler enough for inference to work. 这似乎足以使编译器无法混淆,从而无法进行推理。

So here's the best we have for now: 所以这是我们目前拥有的最好的:

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)

To address your issues: 要解决您的问题:

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

Are you sure this is too restrictive? 您确定这太严格了吗? There's a lot of machinery in TypeScript around trying to determine when literal types should be left narrow or widened . TypeScript中有很多机制可以尝试确定何时应该缩小或扩展文字类型 I think Array<1 | "a"> 我认为Array<1 | "a"> Array<1 | "a"> is a perfectly reasonable type to infer for the value [1, "a"] . Array<1 | "a">是一种推断值[1, "a"]的完全合理的类型。 If you want to widen it, you can tell the compiler that 1 and 'a' are not meant to be literal: 如果要扩大它,可以告诉编译器1'a'并非字面意思:

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

If you really want to force the return type of conditionalArray() to always be widened, then you can use a conditional type like this: 如果您确实想强制将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>

That works, but it might be more complicated than it's worth. 那行得通,但是可能比它的价值更复杂。


Your next issue: 下一个问题:

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

How important is it to you that the compiler recognize when you pass a literal false as the condition in addIf() ? 当您在addIf()传递文字 false作为条件时,编译器识别出对您来说有多重要? I would expect that in real world code you would never do this... if you know at compile-time that the condition is false , then you would just leave it out of the array. 我希望在现实世界的代码中您永远不会这样做...如果您在编译时知道条件为false ,那么您只需将其排除在数组之外。 Conversely, if at compile-time you're not sure whether the condition is true or false , then you should want a type like the above even if the value happens to contain only numbers. 相反,如果在编译时不确定条件是true还是false ,那么即使恰好只包含数字,也应该使用上述类型

However, again, you can force the compiler to go through such logic via conditional types: 但是,再次,您可以通过条件类型强制编译器通过以下逻辑:

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>

That works, but again, it might be more complicated than it's worth. 这行得通,但同样,它可能比它的价值更复杂。


UPDATE 2: 更新2:

Assuming you want to forget about literal false and you'd like to keep the element type as narrow as possible in all cases, you can do something like this: 假设您想忘记字面值false并且希望在所有情况下都将元素类型保持为尽可能 ,则可以执行以下操作:

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

That's because having string , number , and boolean mentioned explicitly inside the generic constraint for T gives the compiler a hint that a literal type is desired. 这是因为在T通用约束中明确提到了stringnumberboolean可以使编译器获得需要文字类型的提示。

See Microsoft/TypeScript#10676 for more details about how and when the compiler chooses to widen or preserve literal types. 有关编译器如何选择以及何时选择扩展或保留文字类型的更多详细信息,请参见Microsoft / TypeScript#10676


Okay, hope that helps. 好的,希望对您有所帮助。 Good luck! 祝好运!

Possible solution in TypeScript (modules export/import removed) 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")
])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM