microsoft/TypeScript

Intellisense and generated declarations emit wrong types (mapped types)

gcanti opened this issue ยท 13 comments

TypeScript Version: 2.1.5, 2.1.6, 2.2.0-dev20170203

VS Code Version: 1.9.1

Code

class Type<T> { t: T }

type TypeOf<T extends Type<any>> = T['t'];

type Props = { [key: string]: Type<any> };

declare function obj<P extends Props>(props: P): Type<{ [K in keyof P]: TypeOf<P[K]> }>;

const str = new Type<string>()

const A = obj({
  a: str
})

type TypeOfA = TypeOf<typeof A> // => Intellisense correctly shows { a: string }

const B = obj({
  b: obj({
    c: str
  })
})

type TypeOfB = TypeOf<typeof B> // => should be { b: { c: string } } but Intellisense shows { b: any; }

// though the inferred type seems correct
const b1: TypeOfB = {} // error
const b2: TypeOfB = { b: {} } // error
const b3: TypeOfB = { b: { c: 'foo' } } // NO error

Expected behavior:

Intellisense should show

type TypeOfB = {
  b: {
    c: string
  };
}

Actual behavior:

Intellisense shows

type TypeOfB = {
  b: any;
}

EDIT: detail screenshot

intellisense

Something's going on with the display writer. I found what I believe is the culprit in checker.ts:

else if (contains(symbolStack, symbol)) {
    // If type is an anonymous type literal in a type alias declaration, use type alias name
    const typeAlias = getTypeAliasForTypeLiteral(type);
    if (typeAlias) {
        // The specified symbol flags need to be reinterpreted as type flags
        buildSymbolDisplay(typeAlias, writer, enclosingDeclaration, SymbolFlags.Type, SymbolFormatFlags.None, flags);
    }
    else {
        // Recursive usage, use any
        writeKeyword(writer, SyntaxKind.AnyKeyword);
    }
}

Context:

// Since instantiations of the same anonymous type have the same symbol, tracking symbols instead
// of types allows us to catch circular references to instantiations of the same anonymous type

At the very least, all the other language service support still works as intended, but obviously this is a bummer. @ahejlsberg might be able to look at it, but I don't know if that'll be be before the 2.2 release.

What's the new milestone?

I don't think this is just intellisense. My generated declarations also show the same:

https://unpkg.com/twitter-api-ts@0.0.28/target/types.d.ts on line 246.

errors refers to this type: https://github.com/OliverJAsh/twitter-api-ts/blob/b86922db500225e68824764bde389087c3867388/src/types.ts#L39

I have tested further and this issue does seem to effect generated declarations as well. Here is a reproducible test case.

Given:

export class Type<T> {
    t: T;
}

export type TypeOf<T extends Type<any>> = T['t'];

type Props = { [key: string]: Type<any> };

declare function obj<P extends Props>(props: P): Type<{ [K in keyof P]: TypeOf<P[K]> }>;

const str = new Type<string>();

const A = obj({
    a: str,
});

export const B = obj({
    b: obj({
        c: str,
    }),
});

export type TypeOfB = TypeOf<typeof B>;

When running tsc --declaration, the generated declaration file is:

export declare class Type<T> {
    t: T;
}
export declare type TypeOf<T extends Type<any>> = T['t'];
export declare const B: Type<{
    b: any;
}>;
export declare type TypeOfB = TypeOf<typeof B>;

Given:

import * as t from 'io-ts';

export const NestedInterface = t.type({
    foo: t.string,
    bar: t.type({
        baz: t.string,
    }),
});

export type NestedInterfaceType = t.TypeOf<typeof NestedInterface>;

When running tsc --declaration, the generated declaration file is:

import * as t from 'io-ts';
export declare const NestedInterface: t.InterfaceType<{
    foo: t.StringType;
    bar: t.InterfaceType<{
        baz: t.StringType;
    }, {
        baz: string;
    }, {
        baz: string;
    }, t.mixed>;
}, {
    foo: string;
    bar: any;
}, {
    foo: string;
    bar: any;
}, t.mixed>;
export declare type NestedInterfaceType = t.TypeOf<typeof NestedInterface>;

Any plans to fix this? It breaks io-ts and runtypes rather severely.

Not sure really what can be done here. there is an anonymous mapped type involved in these cases, the compiler infers a type, and has no way of tracing back its steps to know where it came from..

If you give that mapped type a name, the compiler can then go track it back to its sources, e.g.

type Mapped<P extends Props> = { [K in keyof P]: TypeOf<P[K]> }

declare function obj<P extends Props>(props: P): Type<Mapped<P>>;

It is worth nothing that the compiler has no problems understanding the type, it is just that it can nto write it back the same way it was; and in these cases, it uses any.

If you give that mapped type a name, the compiler can then go track it back to its sources, e.g.

Why can't the compiler do that automatically?

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

It doesn't seem like it's ever a satisfactory outcome that the compiler silently infers any. Or rather, inferring any should always result in an error under noImplicitAny, no?

@mhegazy Could you please answer the question that was left hanging there?

If you give that mapped type a name, the compiler can then go track it back to its sources, e.g.

Why can't the compiler do that automatically?

There hasn't even been a discussion with the core team on how to overcome this "Design Limitation". Either this issue should be reopened for discussion, or the label should in fact say "Won't Fix, Not Our Priority, Go Away".

The type goes through a transformation, the keys are extracted separately into a union of literal types (e.g. "a" | "b"), then a new type that is created by iterating on the literals to generate properties... once that decomposition happens, the compiler really has no way to know where did this property/type originate. if you do have proposals on how this can be enabled we would love to discuss it more.

@mhegazy Thanks! It would be nice if you gave some pointers into the code where this transformation happens, where the new type is created, etc.

in https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts, look for resolveMappedTypeMembers, this is where the mapped type is created.