英文:
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:
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. 🤔🤔🤔🤔🤔
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:
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. 🤔🤔🤔🤔🤔
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论