TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

index signature is missing when do object destructuring to separate variable

Open rifler opened this issue 4 years ago • 10 comments

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

rifler avatar Dec 17 '20 22:12 rifler

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

RyanCavanaugh avatar Dec 23 '20 21:12 RyanCavanaugh

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

Playground

2A5F avatar Dec 29 '20 04:12 2A5F

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

paulboocock avatar Mar 06 '21 17:03 paulboocock

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

kirpichenko avatar Jul 23 '21 13:07 kirpichenko

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

jcalz avatar Oct 11 '21 15:10 jcalz

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 avatar Feb 21 '22 19:02 jcalz

@jcalz can you post a code sample so I can be sure I'm understanding the question?

RyanCavanaugh avatar Feb 22 '22 18:02 RyanCavanaugh

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; }'

Playground link

jcalz avatar Feb 22 '22 19:02 jcalz

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.

RyanCavanaugh avatar Feb 23 '22 00:02 RyanCavanaugh

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"]

Playground

My use case was adding some defaults to the index signature object.

sybereal avatar Oct 20 '22 10:10 sybereal