在TypeScript中访问与泛型类型相关的值
trait Foo<F> {
const FOO: F;
}
struct Bar;
impl Foo<u8> for Bar {
const FOO: u8 = 1;
}
fn foo<T, F>() -> F
where
T: Foo<F>,
{
T::FOO
}
fn main() {
println!("{}", foo::<Bar, _>()); // Prints: 1
}
在上面的Rust代码中,fn foo 可以从泛型类型 T 中访问值 FOO,我想用TypeScript实现类似的效果。
I have tried the code below, but it does not work:
interface Foo<F> {
foo: F;
}
class Bar {
foo: 1 = 1;
}
function foo<T extends Foo<F>, F>(): F {
return T.foo; // 'T' only refers to a type, but is being used as a value here.(2693)
}
console.log(foo<Bar, 1>());
And I have to pass a value foo_ to make the above code work:
interface Foo<F> {
foo: F;
}
class Bar {
foo: 1 = 1;
}
function foo<T extends Foo<F>, F>(foo_: T): F {
return foo_.foo;
}
console.log(foo<Bar, 1>({foo: 1}));
I have two questions about this in particular:
- 有没有办法让
T.foo在不传入实际的T值的情况下就能被function foo访问,就像在Rust代码中那样?也就是说,如何将一个类型参数传递给一个函数,让该函数实际上能够读取它?顺便提一句,我读过 这个回答,与我的情况相当相似,但那不允许函数foo对类型T进行泛化。 - 我知道,当代码被转译成JavaScript时,所有类型信息都会被擦除,这也是为什么
return T.foo不能被转译成JavaScript,从而导致错误。然而,转译器应该能够推断出:1) 给定任意类型T extends Foo<F>,该类型T上必须有一个名为foo、类型为F的字段;以及2) 对于任意类型为Bar的值,它必须具有字段foo,这使得Bar满足接口Foo,且字段foo始终为1。有了这些知识,转译器在看到foo<Bar, 1>()时应该能够输出类似foo(Bar.foo)的东西,甚至foo(1)。我可以让TypeScript转译器做出这样的推断吗?如果可以,该如何实现?如果不能,为什么?
解决方案
As asked what you are trying to do is intentionally impossible in TypeScript.
As you are apparently aware, TypeScript's type system is erased upon compilation to JavaScript. Indeed, for most (but unfortunately not all) features of TypeScript, the compilation step to a sufficiently new version of JavaScript is only type erasure: simply remove all the type-level code (the annotations and interfaces) and your TypeScript is now JavaScript! This makes the runtime behavior of TypeScript code tractable and straightforward to reason about. It completely separates the concerns of type checking from code emitting.
This is all intentional and one of the foundational goals of TypeScript. Inversely, adding any kind of type-system information to the emitted code is an explicitly-stated non-goal of TypeScript (specifically Non Goal #5). So what you are trying to do is explicitly and intentionally not supported.
The "right" way to think about TypeScript code is to imagine writing pure JavaScript code, and then using the type system to help you make sure that your code is going to do the right thing at runtime. You use TypeScript's type system to describe what JavaScript does, not to modify what it does. Type inference makes it easier to write that code, but it doesn't provide values. If you need a value at runtime, you have to provide that value. TypeScript's type system can't and won't do that for you.
The conventional approach to your example therefore looks more like:
interface Foo<F> {
foo: F;
}
class Bar {
foo: 1 = 1;
}
function foo<T extends Foo<unknown>>(foo_: T): T["foo"] {
return foo_.foo;
}
const result = foo(new Bar())
// ^? const result: 1
console.log(result);
Here your foo() function is generic in only one type parameter, T. The type parameter you named F can be computed from T using the indexed access type T["foo"]. When you call foo() you could simply pass in a new value {foo: 1}, but then it has nothing to do with your Bar class. If you want there to be some relationship to your actual Bar class, then you should pass in an actual Bar instance. So foo(new Bar()) (or, instead of new Bar(), some instance of Bar you have around somewhere). That gives you the expected and desired result, where foo(new Bar()) produces a value of type 1 at runtime.
Again, TypeScript is just describing JavaScript for you. If you want that value of type 1 at runtime you need to put it somewhere. If foo() is supposed to represent what happens when you index into a value of type T with a key of "foo", then you need to give it such a value and index into it with a key of "foo". TypeScript is able to see what it will do and infer types for you so you don't have to explicitly write foo<Bar>(new Bar()). But TypeScript cannot, will not, should not -- change what it will do.
A value of type 1 is presumably easy to get and create on demand. If the value were more complicated and expensive, then I'd imagine you'd have that as a constant somewhere to begin with and then just keep referring to it:
const val = { a: 1, b: 2, c: 3 }; // something more complicated
class Baz {
foo = val;
}
const r2 = foo(new Baz());
// ^? const r2: { a: number; b: number; c: number; }
But then at that point foo and Baz seem like a longwinded way to write
const r2 = val;
which means you'd have to be more thoughtful about what you actually want TypeScript to help you do here. Is it important that some Foo<F> somewhere in the universe has a property of some specific type F even if you never want to actually use the value of type Foo<F>? Maybe you don't need foo and your classes at all. Or maybe you do need them but the information you thought was just TypeScript type information really needs to be some kind of Javascript schema object:
interface FooMaker<F> {
schema: { F: F };
new(): Foo<F>
}
class Bar {
static readonly schema = { F: 1 } as const
foo = Bar.schema.F
}
function schemaF<F>(ctor: FooMaker<F>): F {
return ctor.schema.F
}
const result = schemaF(Bar);
// ^? const result: 1
The specifics aren't super important here, but the idea is you need to put all the information you care about into JavaScript and then you can use TypeScript to keep track of it, so you don't need to do things like instantiate a class when you don't actually want an instance of a class.
At this point I'm just speculating about the underlying use case so I'll stop. If another question post appears with more information I'll look at that one.