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
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/[email protected]/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.
Most helpful comment
What's the new milestone?