typescript 当声明一个泛型项数组时,如何允许推断泛型参数?

izkcnapc  于 2023-04-22  发布在  TypeScript
关注(0)|答案(3)|浏览(122)

我有一个情况,我有一个泛型项数组(Item),在项本身中,我希望泛型参数是推断的和特定的。
也就是说,我想有一个泛型项的数组,但是每个泛型项可以有不同的泛型类型,并且应该保留。

type Item<T> = {
  value: T; 
  fn: (value: T) => void; 
}

function usesItem<T>(item: Item<T>) {

}

// This is fine - the value is inferred
usesItem({
  value: 999, 
  fn: (value) => {
      //value is inferred as number
  }
})

第一种方法,声明一个Array<Item<unknown>

function usesItems1(items: Array<Item<unknown>>) {

}

usesItems1([{
  value: 999, 
  fn: (value) => {
    // value is unknown - we want it to be number
  }
}
])

第二种方法:从函数中引入泛型:

function usesItems2<T>(items: Array<Item<T>>) {

}

// appears to work...
usesItems2([{
  value: 999, 
  fn: (value) => {
    // value is number
  }
}
])

// ...but it doesn't really
usesItems2([{
  value: 999, 
  fn: (value) => {
    // value is number
  }
}, {
  // Type 'string' is not assignable to type 'number'.(2322)
  value: "aaa", 
  fn: (value) => {

  }
}
]);

第三种方法-使用infer关键字,我尝试了几种方法:

type InferredItem1<T>  = T extends Item<infer R> ? Item<R> : never; 

function usesItems3(items: Array<InferredItem1<unknown>>) {

}

usesItems3([{
  //Type 'number' is not assignable to type 'never'.(2322)

  value: 999, 
  fn: (value) => {
  }
}
]); 


type InferredItem2<T extends Item<unknown>>  = T extends Item<infer R> ? Item<R> : never; 

function usesItems3b(items: Array<InferredItem2<Item<unknown>>>) {

}

usesItems3b([{
  value: 999, 
  fn: (value) => {
    // value is unknown
  }
}
]);

TypeScript Playground
我怎样才能实现我想要的?
或者,如果这是不可能的,我想一个规范的参考Github的问题/类似的进入它。

wko9yo5t

wko9yo5t1#

好吧,我有一些工作,但它的怪异和黑客,我不知道如果我是正确的 * 为什么 * 它的作品。第一部分是直截了当的:createRoutes函数需要是泛型的,以便有一个类型参数可以推断:

function createRoutes<T>(routes: Route<T>[]) {
  // ...
}

奇怪的是requestBodyValidator需要写成方法而不是箭头函数,但是createHandler需要保持箭头函数:

createRoutes([{
  route: "/a",
  method: "get",
  // method, not arrow function
  requestBodyValidator(item): Bar {
    return item as Bar;
  },
  // arrow function
  createHandler: () => {
    // value is inferred as Bar 
    return (value) => {}
  }
}])

Playground链接
我最好的猜测是,通过使requestBodyValidator成为一个方法,在推断整个对象的类型时会考虑它的类型,因为Typescript在这个意义上将方法视为“对象的一部分”。Typescript将其视为子表达式,并且其类型是从上下文推断的。的类型被推断,那么它的类型就不能被用来推断对象的类型。也许是别的原因

rbl8hiat

rbl8hiat2#

  • 你正在寻找的是存在类型。虽然TypeScript目前不直接支持存在类型(大多数具有泛型类型的语言也不支持),但有间接的方法可以实现它。在TypeScript GitHub存储库上已经有this discussion,但在过去的一年中没有看到太多的活动。*
  • TypeScript中的泛型是通用的,这意味着如果我定义一个函数为<T>function(i: T) {...},它必须适用于所有可能的类型T
  • 你需要的数组需要存在泛型类型。这意味着对于同一个函数<T>function(i: T) {...},函数定义决定了它适用的一组类型,当你调用函数时,你需要考虑所有可能的类型。是的,这有点难以理解。*

以上斜体文字是jcalz这篇文章的释义。
你可以在TypeScript中用回调实现存在泛型。这是你的代码,重写后允许你的异构数组。
这是您的Item类型:

type Item<T> = {
  value: T; 
  fn: (value: T) => void; 
}

现在我们为Item创建一个存在泛型版本:

type ExistentialItem = <U>(callback: <T>(item: Item<T>) => U) => U;

不,ExistentialItem是接受回调并返回回调结果的函数类型。回调接受T类型的Item并返回结果。注意,ExistentialItem的类型不再依赖于Item的类型。您需要将所有Item转换为ExistentialItem,因此为此编写一个helper。

const getExistentialItem = <T>(item: Item<T>): ExistentialItem => {
    return (callback) => callback(item);
}

// The compiled JS for the sake of simplicity
const getExistentialItem = (i) => {
    return (cb) => cb(i);
};

现在,您可以使用ExistentialItem将异构阵列键入为:

const arr: ExistentialItem[] = [
    getItem({
        value: 1,
        fn: (n) => {
            console.log(n.toString())
        }
    }),
    getItem({
        value: '1',
        fn: (n) => {
            console.log(n.padStart(10, '1'))
        }
    }),

]

这个数组的所有类型检查都通过了。需要注意的是,至少在我看来,使用这个ExistentialItem数组要做更多的工作。
假设你希望你的useItems返回一个数组,其中包含数组中所有对象的键。而不是像你通常写的那样,如果它是一个同构数组:

const useItems = (items: Item<number>[]) => {
    return items.map(i => i.key)
}

你必须先调用ExistentialItem

const useItems = (items: ExistentialItem[]) => {
    return items.map(cb => cb(i => i.key))
}

这里我们传递回调,在回调中我们决定如何处理项,而不是直接使用项。
这取决于你决定是否需要对异构数组进行类型检查。如果你知道数组的大小,你可以使用[Item<number>, Item<string>],这是一个不错的解决方案,但是然后推到数组或试图访问大于2的索引会导致问题。或者你可以使用any,并使用类型Assert再次进行类型检查。这不是一个好的解决方案。
因此,让我们只希望存在类型在某个时候被实现到TS中。
为了完整起见,我们再看一遍整个代码:

type Item<T> = {
    key: T
    fn: (val: T) => void
}

type ExistentialItem = <R>(cb: <T>(item: Item<T>) => R) => R;

const getExistentialItem = <T,>(i: Item<T>): ExistentialItem => {
    return (cb) => cb(i);
}

const arr: ExistentialItem[] = [
    getExistentialItem({
        key: 1,
        fn: (n) => {
            console.log(n.toString())
        }
    }),
    getExistentialItem({
        key: '1',
        fn: (n) => {
            console.log(n.padStart(10, '1'))
        }
    }),
]

const useItems = (items: ExistentialItem[]) => {
    return items.map(item => item(i => i.key))
}

同时检查this answerthis answer,以更好地解释通用类型和存在类型。

w51jfk4q

w51jfk4q3#

让我知道它是否适合你:

type Handler<T> = (value: T) => void;

type Methods = "post" | "get" | "put" | "patch" | "delete"

export type Route<T> = {
  methods: Methods,
  requestBodyValidator: (item: unknown) => T;
  createHandler: () => Handler<T>;
}

type CreateHandler<T> = { createHandler: () => Handler<T>; }

type Iterate<T extends ReadonlyArray<Route<any>>, Result extends any[] = []> =
  (T extends []
    ? Result
    : (T extends [infer H, ...infer Tail]
      ? (H extends Route<infer E>
        ? Tail extends ReadonlyArray<Route<any>>
        ? Iterate<Tail, [...Result, Omit<H, 'createHandler'> & CreateHandler<E>]>
        : never
        : never)
      : T)
  )

function createRoutes<
  Type,
  RouteElem extends Route<Type>,
  Routes extends RouteElem[],
  >(routes: Iterate<[...Routes]>) {

}

type Bar = {
  bar: string;
}
type Foo = {
  foo: string;
}

createRoutes([{
  methods: 'get',
  requestBodyValidator(item: unknown): Bar {
    return item as Bar;
  },
  createHandler: () => {

    // I want value to be inferred as Bar 
    return (value /** Bar */) => {

    }
  }
}, {
  methods: 'post',
  requestBodyValidator(item: unknown): Foo {
    return item as Foo;
  },
  createHandler: () => {

    return (value /** Foo */) => {

    }
  }
}])

Playground

**P.S.我无耻地窃取了@卡亚关于使用方法而不是箭头函数的想法。

我不能用箭使它工作

相关问题