Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Star-imported module should conform to the type of a string-indexed object #17622

Closed
pelotom opened this issue Aug 4, 2017 · 11 comments
Closed
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@pelotom
Copy link

pelotom commented Aug 4, 2017

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!

@pelotom
Copy link
Author

pelotom commented Aug 4, 2017

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

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 6, 2017

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);

@pelotom
Copy link
Author

pelotom commented Aug 6, 2017

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.

@pelotom
Copy link
Author

pelotom commented Aug 6, 2017

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.

@aluanhaddad
Copy link
Contributor

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.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 6, 2017

Duplicate of #16248
Duplicate of #10998

@thw0rted
Copy link

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

// 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.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 17, 2017

@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.

@thw0rted
Copy link

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:

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.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 17, 2017

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

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Aug 17, 2017
@RyanCavanaugh
Copy link
Member

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).

@mhegazy mhegazy added Bug A bug in TypeScript and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Sep 19, 2017
@mhegazy mhegazy added this to the TypeScript 2.6 milestone Sep 19, 2017
@weswigham weswigham added the Fixed A PR has been merged for this issue label Oct 11, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

7 participants