英文:
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<string, number> }
console.log(this.bar); // should be number
console.log(this.baz); // should be Record<string, number>
return "Hello";
},
/*
* World
*/
bar(): number {
console.log(this.foo); // should be string
console.log(this.baz); // should be Record<string, number>
return 123;
},
/*
* World
*/
baz(): Record<string, number> {
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 = () => 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
*/
}
Obviously, I'm getting this: Context<Record<string, any>>
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.foo
、this.bar
和 this.baz
的类型正是你想要的。
英文:
As far as I know the only way to get the kind of behavior you're looking for is to use the "magic" ThisType<T>
utility type. It's "magic" because even though the definition of ThisType<T>
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<M extends object>(input:
M & ThisType<{ [K in keyof M]:
M[K] extends (...args: any) => infer R ? R : M[K]
}>) { }
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) => 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 => x.toFixed()); // okay
return "Hello";
},
bar(): number {
this.foo.toUpperCase(); // okay
Object.values(this.baz).map(x => x.toFixed()); // okay
return 123;
},
baz(): Record<string, number> {
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论