TypeScript
TypeScript copied to clipboard
index signature is missing when do object destructuring to separate variable
TypeScript Version: 4.1.2
Search Terms: destructuring, index signature is missing
Expected behavior: it works
Actual behavior: when I do object destructuring to separate variable index signature is lost somewhere
Related Issues:
Code
type InitialIndexType = {
key: string;
};
type FinalIndexType = Record<string, string | number | boolean | null | undefined>;
const runtimeInitialVariable: InitialIndexType = { key: 'value' };
// 1. ok, InitialIndexType is compatible with FinalIndexType
const runtimeFinalVariable: FinalIndexType = runtimeInitialVariable;
// 2. error with destructuring, but only when do it to separate variable
const { ...runtimeInitialVariable_2 } = runtimeInitialVariable;
const runtimeFinalVariable_2: FinalIndexType = runtimeInitialVariable_2; // Index signature is missing in type '{ key: string; }'.
// 3. ok with destructuring, without separate variable
const runtimeFinalVariable_3: FinalIndexType = { ...runtimeInitialVariable };
// 4. ok even if I copy&paste type from error message and cast destructured variable to it
const { ...runtimeInitialVariable_4 } = runtimeInitialVariable;
const runtimeFinalVariable_4: FinalIndexType = runtimeInitialVariable_4 as { key: string }; // also works as `as InitialIndexType`
Output
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
const runtimeInitialVariable = { key: 'value' };
// ok, InitialIndexType is compatible with FinalIndexType
const runtimeFinalVariable = runtimeInitialVariable;
// error with ...rest, but only when do ...rest to separate variable
const runtimeInitialVariable_2 = __rest(runtimeInitialVariable, []);
const runtimeFinalVariable_2 = runtimeInitialVariable_2;
// ok with rest, without separate variable
const runtimeFinalVariable_3 = Object.assign({}, runtimeInitialVariable);
// ok even if I copy&paste type from error message and cast ...rest variable to it
const runtimeInitialVariable_4 = __rest(runtimeInitialVariable, []);
const runtimeFinalVariable_4 = runtimeInitialVariable_4; // also works as `as InitialIndexType`
Compiler Options
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"esModuleInterop": true,
"declaration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": 2,
"target": "ES2017",
"jsx": "React",
"module": "ESNext"
}
}
Playground Link: Provided
This behavior falls out of the inference rules and it's not clear to me which one could be changed given the current type mechanics available. runtimeInitialVariable_2 would have need to have a sealed/exact type (#12936) to be safely assigned to InitialIndexType
Shorter code
let { ...obj } = { a: 1 }
let foo: Record<string, number> = obj
// ^
// Type '{ a: number; }' is not assignable to type 'Record<string, number>'.
// Index signature is missing in type '{ a: number; }'.(2322)
let { a } = { a: 1 }
let foo: Record<string, number> = { a }
// ^ this ok
I've just bumped into this one as well. It feels to me like the destructured object should be assignable to Record<string, unknown>.
Perhaps interestingly, it turns out if you spread the object into a new one it works whilst the original does not 🤷🏻
const { prop1, ...rest } = { prop1: 'a string', prop2: 2 };
const options1: Record<string, unknown> = rest; // This doesn't work
const options2: Record<string, unknown> = {...rest}; // This does work
In addition to the last comment if you use any instead of unknown it somehow works 😕
const { prop1, ...rest } = { prop1: 'a string', prop2: 2 };
const options1: Record<string, unknown> = rest; // This doesn't work
const options2: Record<string, unknown> = {...rest}; // This does work
const options3: Record<string, any> = rest; // This does work
const options4: Record<string, any> = {...rest}; // This does work
Now that we have pattern template literal index signatures, this is even more noticeable:
interface Foo {
x: string,
[k: `y${string}`]: number;
}
declare const foo: Foo;
const bar = { ...foo, z: true };
// const bar: { z: boolean; x: string; } ... no index signature here
Can someone comment on why we're not allowing a destructured rest element to have an implicit index signature? The argument in #15300 against doing this for interfaces (i.e., declaration merging issues) doesn't seem to apply here.
@jcalz can you post a code sample so I can be sure I'm understanding the question?
Oh, sure, something like this:
const foo = (x: Record<string, unknown>) => void 0
const x = { a: 0 }
// const x: { a: number }
foo(x); // okay (implicit index signature, right?)
const { ...y } = x;
// const y: { a: number }
foo(y); // error (no implicit index signature)
// ~ <-- Index signature for type 'string' is missing in type '{ a: number; }'
The current rule for implicit index signatures is that the originating declaration must be an object type literal, or inferred from an actual object literal (let's call this "objectish"). IOW, types coming from classes or interfaces do not qualify:
declare class C {
// In general we cannot assume this list
// of declarations to be exhaustive
a: number;
}
// Error (correct)
foo(new C());
const { ...z } = new C();
// Error (correct)
foo(z);
const { ...z } makes z look objectish because it's using { }, but it's really not, since it can source properties from types which aren't objectish.
So I think a reasonable proposal would be to grant z obectishness iff its initializing expression is objectish -- that's no less safe than the current rules, even if other properties are picked out. Happy to take a new suggestion issue on that and we can probably just prototype it to see what happens.
Not sure if it's the same or a related issue, but I just ran into something that feels similar: spreading an object with an index signature loses the index signature on the result type.
type Foo = {[key: string]: string}
const foo: Foo = {a: "b", c: "d"}
const bar = {e: "f", g: "h"}
const baz = {...bar, ...foo}
// Should work, but index signature disappears during spread
const a = baz["a"]
My use case was adding some defaults to the index signature object.