在TypeScript中转换输入对象类型

huangapple go评论65阅读模式
英文:

Converting input object type in TypeScript

问题

我需要交替更改传递给函数的对象的类型,以便对象内的每个方法都接收修改后的 this

create({
  /*
   * Hello
   */
  foo(): string {
    console.log(this); // 应该是 { foo: string, bar: number, baz: Record<string, number> }
    console.log(this.bar); // 应该是 number
    console.log(this.baz); // 应该是 Record<string, number>
    return "Hello";
  },


  /*
   * World
   */
  bar(): number {
    console.log(this.foo); // 应该是 string
    console.log(this.baz); // 应该是 Record<string, number>
    return 123;
  },

  /*
   * World
   */
  baz(): Record<string, number> {
    console.log(this.foo); // 应该是 string
    console.log(this.bar); // 应该是 number
    return {
      foo: 123,
      bar: 456,
    };
  },
});

对于 create 函数,我尝试了以下方法,但并不成功:

type PropMethod = () => any;

type Property<P extends PropMethod = any> = P extends PropMethod ? P : never;

type Context<T extends Record<string, Property>> = {
  [K in keyof T]: T[K] extends Property ? ReturnType<T[K]> : never;
};

type Properties<P extends Record<string, Property>> = {
  [K in keyof P]: P[K] extends (...args: infer Args) => infer R
    ? (this: Context<P>, ...args: Args) => R
    : never;
};

function create<
  C extends Record<string, Property>,
>(input: Properties<C>) {
  /**
   * Body implementation
   */
}

显然,我得到了 this: Context<Record<string, any>>,而函数内的每个属性也都是 any

是否有可能使用 TypeScript 实现这种类型?其中一个要求是不应该有硬编码的类型,并且整个东西都是一个抽象。

此外,如果有人能提出一个更好的问题标题,那就太棒了。

英文:

I need to alternate the type of an object passed to a function, so every method inside the object receive a modified this:

create({
  /*
   * Hello
   */
  foo(): string {
    console.log(this); // should be { foo: string, bar: number, baz: Record&lt;string, number&gt; }
    console.log(this.bar); // should be number
    console.log(this.baz); // should be Record&lt;string, number&gt;
    return &quot;Hello&quot;;
  },


  /*
   * World
   */
  bar(): number {
    console.log(this.foo); // should be string
    console.log(this.baz); // should be Record&lt;string, number&gt;
    return 123;
  },

  /*
   * World
   */
  baz(): Record&lt;string, number&gt; {
    console.log(this.foo); // should be string
    console.log(this.bar); // should be number
    return {
      foo: 123,
      bar: 456,
    };
  },
});

For the create function I tried the following approach without much of a success:

type PropMethod = () =&gt; any;

type Property&lt;P extends PropMethod = any&gt; = P extends PropMethod ? P : never;

type Context&lt;T extends Record&lt;string, Property&gt;&gt; = {
  [K in keyof T]: T[K] extends Property ? ReturnType&lt;T[K]&gt; : never;
};

type Properties&lt;P extends Record&lt;string, Property&gt;&gt; = {
  [K in keyof P]: P[K] extends (...args: infer Args) =&gt; infer R
    ? (this: Context&lt;P&gt;, ...args: Args) =&gt; R
    : never;
};

function create&lt;
  C extends Record&lt;string, Property&gt;,
&gt;(input: Properties&lt;C&gt;) {
  /**
   * Body implementation
   */
}

Obviously, I'm getting this: Context&lt;Record&lt;string, any&gt;&gt; and every property inside the functions is any too.

Is it even possible to achieve such typing with TS? One of the requirements is that there should be no hardcoded types and as the whole thing is an abstraction.

Also, it'd be awesome if someone could suggest a better title for the question.

答案1

得分: 0

根据我所知,要实现你所需的行为,唯一的方法是使用 "magic" ThisType<T> 实用类型。它被称为 "magic",因为尽管 ThisType<T> 的定义 只是一个空接口,但编译器会特殊处理它,以指示希望方法的 this 上下文被视为的类型。

以下是我如何在你的代码中使用它的方式:

function create<M extends object>(input:
  M & ThisType<{ [K in keyof M]:
    M[K] extends (...args: any) => infer R ? R : M[K]
  }>) { }

所以我在说 input 的类型是泛型 M,但它的属性的 this 上下文应该被推断为

{ [K in keyof M]: M[K] extends (...args: any) => infer R ? R : M[K] }

在这里,我们通过条件类型推断M 的方法映射到它们的返回类型。


让我们来测试一下:

create({
  foo(): string {
    this.bar.toFixed(); // 可以
    Object.values(this.baz).map(x => x.toFixed()); // 可以
    return "Hello";
  },
  bar(): number {
    this.foo.toUpperCase(); // 可以
    Object.values(this.baz).map(x => x.toFixed()); // 可以
    return 123;
  },
  baz(): Record<string, number> {
    this.foo.toUpperCase(); // 可以
    this.bar.toFixed(); // 可以
    return {
      foo: 123,
      bar: 456,
    };
  },
});

看起来不错!this.foothis.barthis.baz 的类型正是你想要的。

Playground 上的代码链接

英文:

As far as I know the only way to get the kind of behavior you're looking for is to use the "magic" ThisType&lt;T&gt; utility type. It's "magic" because even though the definition of ThisType&lt;T&gt; it is just an empty interface, it is special-cased by the compiler to indicate the type you want the this context of methods to be treated as.

Here's how I'd use it for your code:

function create&lt;M extends object&gt;(input:
  M &amp; ThisType&lt;{ [K in keyof M]:
    M[K] extends (...args: any) =&gt; infer R ? R : M[K]
  }&gt;) { }

So I'm saying that the type of input is the generic M, but that the this context of its properties should be inferred as

{ [K in keyof M]: M[K] extends (...args: any) =&gt; infer R ? R : M[K] }

where we're mapping the methods of M to just their return types via conditional type inference


Let's test it out:

create({
  foo(): string {
    this.bar.toFixed(); // okay
    Object.values(this.baz).map(x =&gt; x.toFixed()); // okay
    return &quot;Hello&quot;;
  },
  bar(): number {
    this.foo.toUpperCase(); // okay
    Object.values(this.baz).map(x =&gt; x.toFixed()); // okay
    return 123;
  },
  baz(): Record&lt;string, number&gt; {
    this.foo.toUpperCase(); // okay
    this.bar.toFixed(); // okay
    return {
      foo: 123,
      bar: 456,
    };
  },
});

Looks good! The types of this.foo, this.bar, and this.baz are exactly what you wanted them to be.

Playground link to code

huangapple
  • 本文由 发表于 2023年7月10日 22:23:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/76654688.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定