TypeScript Version: 3.9.2
Search Terms:
incorrectly inferred string index type
inferred object property in function return type
Expected behavior:
The key values in each Variation should be inferred as string not the literal string value
Actual behavior:
The key values in each Variation are inferred as the literal string value of the first object
Related Issues:
Possibly related: https://github.com/microsoft/TypeScript/issues/38759,
Code
type Variation = {
[testDescription: string]: () => void;
}
export type VariationProvider = {
variations: Variation[];
};
export type VariationProviderFactory = (extras: object) => VariationProvider;
export function createVariationProvider(factory: VariationProviderFactory) {
return factory;
}
createVariationProvider(() => {
return {
variations: [
{
foo: () => {}
},
{
broken: () => {}
},
]
}
})
Output
export function createVariationProvider(factory) {
return factory;
}
createVariationProvider(() => {
return {
variations: [
{
foo: () => { }
},
{
broken: () => { }
},
]
};
});
Compiler Options
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"useDefineForClassFields": false,
"alwaysStrict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"downlevelIteration": false,
"noEmitHelpers": false,
"noLib": false,
"noStrictGenericChecks": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"preserveConstEnums": false,
"removeComments": false,
"skipLibCheck": false,
"checkJs": false,
"allowJs": false,
"declaration": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"target": "ES2017",
"module": "ESNext"
}
}
Playground Link: Provided
@ahejlsberg the optional object literal property detection logic isn't quite doing the right thing here
type VariationProviderFactory = () => Record<string, () => void>[]
const variationProviderFactory: VariationProviderFactory = () => [
{
foo: () => { }
},
{
broken: () => { }
},
]
const vars: Record<string, () => void>[] = [
{
foo: () => { }
},
{
broken: () => { }
}
];
const variationProviderFactory2: VariationProviderFactory = () => vars
The object literal
[
{ foo: () => {} },
{ broken: () => {} }
]
has the widened type
[
{ foo: () => void, broken?: undefined },
{ broken: () => void, foo?: undefined }
]
The optional properties are a result of object literal normalization (#19513), which is part of type widening. So, we have the unfortunate situation that the unwidened form of the object literal is assignable to Record<string, () => void>, whereas the widened form of the object literal isn't. The examples then simply reveal when widening is or isn't performed. And, basically, we widen function return expression types unless the containing function has an explicit type annotation--and the examples don't.
Anyway, I think the best fix is to change the Record<string, () => void> type to Record<string, (() => void) | undefined> which really is more correct. You could even argue that we're finding a potential bug, except we're not particularly consistent about it.
Most helpful comment
A more distilled version