TypeScript 介绍一种在不丢失泛型类型信息的情况下操作函数类型的机制,

9rbhqvlz  于 4个月前  发布在  TypeScript
关注(0)|答案(4)|浏览(89)

建议

🔍 搜索词

  • 通用函数添加参数
  • 通用函数的参数
  • 在通用函数前添加参数

✅ 可实现性检查清单

我的建议符合以下指导原则:

  • 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同的JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
  • 这个特性将与TypeScript's Design Goals的其他部分保持一致。

⭐ 建议

能够在不失去泛型类型参数的情况下操作泛型函数类型将会非常有用(参见下面的激励示例)。想出一种好的语法并不容易,但我看到了几种可能的方法(精确的语法/名称可能会有所改变):

  1. 一堆实用类型(不雅观但能完成任务)
type F = <T, R>(t: T) => R;

type X1 = WithReturnType<F, number>; // X1 = <T, R>(t: T) => number;
type X2 = WithParameters<F, [x: number]>; // X1 = <T, R>(x: number) => R;
type X3 = PrependParameter<F, number>; // X1 = <T, R>(arg0: number, t: T) => R;
type X4 = AppendParameter<F, number>; // X1 = <T, R>(t: T, arg0: number) => R;
type X5 = ReplaceParameter<F, 0, number>; // X1 = <T, R>(t: number) => R;
  1. extends进行增强(可能是最漂亮的解决方案,但在规范它时可能会带来挑战):
type F = <T, R>(t: T) => R;

type X1 = F extends (...args: infer Args) => infer R
    ? (x: number, ...args: Args) => R
    : never;
// ...
  1. extends使用新的语法来声明意图(这要更广泛得多,但只有在与更广泛的“泛型泛型”或HKT特性结合时才有意义):
type F = <T, R>(t: T) => R;

type X1 = F extends <...infer TypeArgs>(...args: infer Args) => infer R
    ? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
    : never;
// ...

// or maybe
type X1 = F extends <...infer TypeArgs>(...args: infer Args<...TypeArgs>) => infer R<...TypeArgs>
    ? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
    : never;
// ...
  1. 替代现有语法的扩展(一般来说,这里有一个可能性):
type F = <T, R>(t: T) => R;

type X1 = <...Args>(x: number, ...args: Parameters<F<...Args>>) => ReturnType<F<...Args>>;

📃 激励示例

考虑一种类似这样的API/中间件构建器:

builder
    .add('slow', { rate: 500 })
    .add('get', { query: ['abc'] as const }, ({ query: { abc } }) => {
        // ...
    };

当使用时,它可以被类型化为如下所示:

interface Builder {
    add(type: 'slow', options: { rate: number }): Builder;
    add<Q extends readonly string[]>(
        type: 'get',
        options: { query: Q },
        callback: (data: { query: { [K in Q[number]]: string } }) => void
    ): Builder;
    // ...
}

然而,现在我们希望第三方能够能够向构建器中添加自己的中间件类型。为它们提供API相对容易:

BuilderFactory.register('merge', (options, callback) => {
    // ...
    callback(...);
    // ...
} );

但是,类型化它就困难多了。通常,我们会使用模块增强来允许第三方对他们的中间件类型进行类型化:

interface Builder {
    add<T extends keyof BuilderMethods>(type: T, ...args: Parameters<BuilderMethods[T]>): ReturnType<BuilderMethods[T]>
}

// third party code
declare module '...' {
    interface BuilderMethods {
        merge: <Q extends readonly string[]>(
            options: { query: Q },
            callback: (data: { query: { [K in Q[number]]: string } }) => void
        ) => void;
    }
}

但是,如果我们这样做,就会丢失类型参数中编码的所有信息。这个功能将允许add方法保留其派生的函数类型的泛型类型信息。

💻 用例

我的用例与上面的激励示例相当相似。目前我的解决方法是只对第一方方法有一个良好的类型体验,但这并不是理想的情况。

vnjpjtjt

vnjpjtjt1#

目前许多库(fp-ts,effect-ts)在实现类型类方面存在一些问题。它们需要预先为所有函数添加泛型参数,以便在链式调用时不会丢失泛型类型信息。如果能像操作数组那样操作泛型函数别名就好了。下面是一个实现处理最多4个参数的Functor的例子。

不要过于关注$,它只是一个带有大量 Package 器的HKT。$<F,[A]>是一个HKT,等同于调用实用类型F<A>。对于数组类型,它执行以下操作:

type Test = $<Kind.Array, [A]>;
// same as
type Test = A[]

所以今天要实现一个固定第一个类型的Functor,让其他参数保持泛型。当想要将Functors组合在一起时,就会开始出现问题。处理所有用例变得指数级复杂。因此,可以借助一些辅助工具来解决这个问题:

export interface Functor {
map: <A, B>(f: (a: A) => B) => <...Arity>(fa: $<F, [A, ...Arity]>) => $<F, [B, ...Arity]>

k2fxgqgv

k2fxgqgv2#

我建议另外两种解决方案:

方案1:

添加一个 AsGeneric<> 工具:

type X = <T,R>(e: T) => R;
type Y<T> = AsGeneric<X, T, string> // Y<T> = <T,string>(e: T) => string

方案2:

允许使用 satisfies 与类型一起使用:

class Base<T> {}
class Child<T> extends Base<T> {}

type X = Child<unknown>
type Y = X satisfies Base<string>; // Y = Child<string>

有了这些,我们就可以使用一些解决方法。

zrfyljdw

zrfyljdw3#

你好,
我找到了一个改进的解决方案,可能在某些情况下有用。通过模拟一种类型Map的方式,类似于之前的解决方法,缺点是我们必须手动注册我们希望操作的类型/形状。对于方法,也许有一种更通用的方法可以使用某种递归实现?
另一个缺点是我们需要设置所有泛型类型。但是也许有一种方法可以添加一些推断?
我们可以使用它来获取一种类型的元组,然后使用Convert重新注入。使用某种递归操作这种类型的元组也是可能的吗?
因此我认为一个更完整/通用的解决方法是可能的?
根据需求,为了进行更严格的检查:
如果需要,结果的联合也可以转换为交集。

q3qa4bjr

q3qa4bjr4#

在其他情况下,也可以使用类似的解决方法:

type P<T> = Parameters<F<T>>
type R<T> = ReturnType<F<T>>

type P2<T> = // some manipulations on P<T>

type F2<T> = <T>(...args: P2<T>) : R<T>

相关问题