Typescript: Star-imported module should conform to the type of a string-indexed object

Created on 4 Aug 2017  路  11Comments  路  Source: microsoft/TypeScript

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)

Suppose I have this module:

// foo.ts

export const x = 3
export const y = 5

Conceptually this module represents a map of strings to numbers, { [k: string]: number }, however if I try to use it directly as such I get a type error:

// bar.ts

import * as foo from './foo'

function f(map: { [k: string]: number }) {
  // ...
}

f(foo)
//^^^ Argument of type 'typeof "/Users/tom/code/example/src/foo"' is not assignable to parameter of type '{ [k: string]: number; }'.
//      Index signature is missing in type 'typeof "/Users/tom/code/example/src/foo"'.

Fortunately there is a workaround: I can simply spread and recompose the module to make it conform to the expected type:

f({ ...foo }) // this type checks!

However it'd be nice if the more straightforward approach just worked!

Bug Fixed

Most helpful comment

We should be able to apply the same treatment of inferred indexers to star-imports as we do for object literals, since (in theory) we know the entire set of members (because ES6 modules are immutable).

All 11 comments

Interestingly, there doesn't seem to be a problem when using a value type of any (typeof map ={ [k: string]: any }).

Since imports are inherently static, why not use

import * as foo from './foo';

declare function bar(map: typeof foo): void;

on a side note, this allows for some erroneous coding patterns such as

// foo.ts
export let x = 1;
// bar.ts
import * as foo from './foo';

function bar(map: typeof foo) {
  map.x = 0; // WAT
}

map(foo);

Since imports are inherently static, why not use

import * as foo from './foo';

declare function bar(map: typeof foo): void;

This was just a minimal repro. In practice, the function I'm trying to pass foo to might be defined elsewhere that knows nothing about foo. It might even be in a 3rd party library which I have no control over.

on a side note, this allows for some erroneous coding patterns such as

// bar.ts
import * as foo from './foo';

function bar(map: typeof foo) {
 map.x = 0; // WAT
}

Yeah, that's an oddity which should probably get its own bug report.

It seems like when treating foo as an object it should be no different than if instead of importing a module we had declared

type Foo = {
  readonly x: number
  readonly y: number
}

let foo: Foo = {
  x: 3,
  y: 5,
}

And in this case,

function bar(map: { [k: string]: number }) {
  // ...
}

bar(foo)

type checks just fine.

Indeed. With respect to reporting a bug about the mutable binding via an indirect reference to a module namespace object, the closest approximation is the readonly modifier which currently doesn't do much. It still probably should be part of the type though.

Duplicate of #16248
Duplicate of #10998

I can't get the workaround from the OP to work with a JS library I'm using:

````ts
// foo.d.ts
declare module "foo" {
class Bar { ... }
}

// app.ts
import * as Foo from "foo";
let f = {...Foo};
let bar = new f["Bar"]; // <-- Flagged with "no index signature" error
````

I just need a way to pass around class constructors to dynamically choose a subclass at runtime. In my legacy (JS) code I did this with string-indexing into the module -- I'm not married to that solution in TS but it seems like it should work.

@thw0rted just in case, did you forget to export Bar?

Nevermind, it works regardless of the presence of export "foo" if declared in a .d.ts file. Just makes sure that declare module "foo" is in global scope, not module scope as in the latter case it will be interpreted as an augmentation and not a declaration.

I've got the wrong end of the stick here. What I'm trying to do is select a class declared in a module using the class name as a string. What I gave above as an example works without any workarounds, the problem is when I make the index parameter a variable instead of a literal. It's complaining that I can't index into the module with any old string, and I don't know how to constrain it such that the variable is forced to be a valid index key. I thought that was keyof but I can't get that to work either.

A better example:

ts import * as Foo from "foo"; let b1 = new Foo["Bar"]; // I was wrong, no compiler error here let k2: string = "Bar"; let b2 = new Foo[k2]; // compiler error, "has no index signature" let k3: keyof Foo = "Bar"; // compiler error, "Cannot find name 'Foo'." let b3 = new Foo[k3]; // I think this would work?

Of course I can avoid this by just casting Foo to any but that kind of defeats the point.

I see what the issue is now.

import * as Foo from "foo";
let k3: keyof Foo = "Bar";  // compiler error, "Cannot find name 'Foo'."

needs to be written as

import * as Foo from "foo";
let k3: keyof typeof Foo = "Bar"; 

since keyof takes a _type_ but Foo is a value. In some situations, Foo may also refer to a type but in the example you have shown, it is only a value.

For example

// foo.d.ts
declare module "foo" {
  type X = {id : number};
  const X: X;
  export = X;
}

We should be able to apply the same treatment of inferred indexers to star-imports as we do for object literals, since (in theory) we know the entire set of members (because ES6 modules are immutable).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

seanzer picture seanzer  路  3Comments

blendsdk picture blendsdk  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments