为什么在具有计算键的映射对象上执行的函数无法推断出正确的类型?

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

Why do functions on mapped objects with computed keys not infer correct types?

问题

对不起,我不能提供代码的翻译。如果您有关代码的任何问题或需要帮助,请随时提问。

英文:

I have a need to re-map the keys of one object - as string literals - into slightly differently formatted keys for an expected object - again, as string literals. I'm using Typescript's template string literals to do this, with version 4.9.5. I'm also taking the value type from the first object and mapping it to the argument of a function in the second object. In my reproduction code below, I also made it the return type, just for extra clarity. For some reason, having an inline expression for a computed key makes the type inference fail in a strange way. Code for reproducing:

type Original = { foo: 'expects a string literal', baz: boolean, bar: number }
type Mapped = {
  [prop in keyof Original as `$(${prop & string})`]: (arg: Original[prop]) => Original[prop]
}

type PropSelector<name extends string> = `$(${name & string})`
const propSelector =  <propName extends string>(propName: propName): PropSelector<propName> => `$(${propName})`

const barKey = propSelector('bar');

const workingTestObject: Mapped = {
  '$(foo)': (arg) => 'expects a string literal',
  '$(baz)': (arg) => true,
  // works just fine as a const (and not inlined)
  [barKey]: (arg) => 51345
}

const correctFailures: Mapped = {
  // Errors correctly for incorrect return types as well
  // Type 'number' not assignable to 'expects a string literal'
  '$(foo)': (arg) => 5552,
  // Type 'string' not assignable to 'boolean'
  '$(baz)': (arg) => '1234',
  // Type 'boolean' not assignable to type 'number'
  [barKey]: (arg) => true
}

Notice above that the barKey const is the result of the function call to propSelector, which outputs '$(bar)', which is also the literal return type. Using the const as a key in the object works exactly as it should.

However, if I simply move the propSelector function call inline, instead of using a separate const, it all falls apart and TS defaults the argument to any, although it is still able to infer the return type just fine for some reason.

const failingTestObject: Mapped = {
  '$(foo)': (arg) => 'expects a string literal',
  '$(baz)': (arg) => true,
  // type check error: `arg` is implicitly `any`?  But return type is fine?
  [propSelector('bar')]: (arg) => 13451
}

// return type errors correctly when using propSelector
// but cannot infer the type for arg, so gives "implicit any" error for all of them
const correctFailures: Mapped = {
  // return type expected string literal, but got number
  [propSelector('foo')]: (arg) => 5552,
  // return type expected boolean, but got string
  [propSelector('baz')]: (arg) => '1234',
  // return type expected number, but got boolean
  [propSelector('bar')]: (arg) => true
}

Here's a playground link of the above code: https://tsplay.dev/WG2Yom

Notice that I'm being very explicit as to the literal type that is returned from the propSelector function which creates the key. So, the correct inference occurs when the object key is a string, or a reference to an expression as a const (like with barKey), but not if the same expression is simply inlined.

I've tried a few things, like different combinations of the & string bit in the literal types, either one or the other, or both, or neither, with no difference in behaviour.

Does anyone know why this is? Is it expected behaviour? I know I can simply specify the argument type for the function inline, but I'm trying to automate as much type acquisition as possible, to avoid duplicate typing, so I'd love to make this work. Having the expression inlined is also important in my case, so just using a const isn't great either.

Many thanks!


Edit: So it turns out there was already a PR to fix this issue, and only days after posting this, it was merged! The PR can be found here: https://github.com/microsoft/TypeScript/pull/51915

答案1

得分: 1

I pushed my link in the comments a bit further, and it seems you can effectively resolve this by giving hints to the compiler which will be type checked. As long as you setup your tsconfig to flag any types, you will have type-safe code:

Playground

type Original = { bar: number }

type MappedC = {
  [prop in keyof Original as `$(${prop})`]: (arg: Original[prop]) => Original[prop]
}

const propSelector = <propName extends string>(propName: propName) => `$(${propName})` as const

const c1: MappedC = {
  [propSelector('bar')]: (arg) => 5, // correct usage, `arg` not inferred. Return type checked & passed.
}

const c2: MappedC = {
  [propSelector('bar')]: (arg) => "I am a string", // incorrect usage, arg not inferred. Return type checked & failed.
}

// Should be equivalent to c1 but not. &#129300;&#129300;&#129300;&#129300;&#129300;
const c3: MappedC = {
  ["$(bar)"]: (arg) => 5, // correct usage, `arg` inferred. Return type checked & passed.
}

// Let's help the type checker

const c4: MappedC = {
  [propSelector('bar')]: (arg: number) => 5, // Correct usage, all good!
}

const c5: MappedC = {
  [propSelector('bar')]: (arg: string) => 5, // Incorrect usage, an error is flagged!
}

In the meantime, I recommend you file an issue against the compiler.

英文:

I pushed my link in the comments a bit further, and it seems you can effectively resolve this by giving hints to the compiler which will be type checked. As long as you setup your tsconfig to flag any types, you will have type-safe code:

Playground

type Original = { bar: number }

type MappedC = {
  [prop in keyof Original as `$(${prop})`]: (arg: Original[prop]) =&gt; Original[prop]
}

const propSelector =  &lt;propName extends string&gt;(propName: propName) =&gt; `$(${propName})` as const


const c1: MappedC = {
  [propSelector(&#39;bar&#39;)]: (arg) =&gt; 5, // correct usage, `arg` not inferred. Return type checked &amp; passed.
}

const c2: MappedC = {
  [propSelector(&#39;bar&#39;)]: (arg) =&gt; &quot;I am a string&quot;, // incorrect usage, arg not inferred. Return type checked &amp; failed.
}

// Should be equivalent to c1 but not. &#129300;&#129300;&#129300;&#129300;&#129300;
const c3: MappedC = {
  [&quot;$(bar)&quot;]: (arg) =&gt; 5, // correct usage, `arg` inferred. Return type checked &amp; passed.
}

// Let&#39;s help the type checker

const c4: MappedC = {
  [propSelector(&#39;bar&#39;)]: (arg: number) =&gt; 5, // Correct usage, all good!
}

const c5: MappedC = {
  [propSelector(&#39;bar&#39;)]: (arg: string) =&gt; 5, // Incorrect usage, an error is flagged!
}

In the meantime, I recommend you file an issue against the compiler.

huangapple
  • 本文由 发表于 2023年2月24日 05:13:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/75550379.html
匿名

发表评论

匿名网友

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

确定