-
Notifications
You must be signed in to change notification settings - Fork 109
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
Feature Request: Support resolved mapped types #888
Comments
Some examples, with expected Tsickle annotations: // This is fully expressible
/** @type {{a: number, b: number}} */
let x: Record<'a'|'b', number>;
class ServiceId<T>{} // Only expand the inner type; the outer type is expressible.
/** @type {!ServiceId<{a: number, b: number}>} */
let x: ServiceId<Record<'a'|'b', number>>; // This type is only partially expressible
/** @typedef {{name: string, metadata: ?}} */
type Spec<T> = {
name: string;
metadata: Record<keyof T, boolean>;
}
// So this must re-specify the entire type to fill in the mapped type
/** @type {{name: string, metadata: {a: boolean}}} */
let x: Spec<{ a: number }>; // In contrast, this type is fully expressible (the mapped type's keys don't use T)
// Ignore my condensed syntax; this would be a full @record type.
/** @record @template T class { ctor() {name: string, samples: {a: T, b: T}}} */
type Options<T> = {
name: string;
samples: Record<'a'|'b', T>;
}
/** @type {!Options<{a: number>}} */
let x: Options<{ a: number }>; // Simpler version of that:
/** @typedef {{name: string, samples: {a: boolean, b: boolean}}} */
type Options = {
name: string;
samples: Record<'a'|'b', boolean>;
};
/** @type {!Options} */
let x: Options; |
The practical use-cases of these distinctions are builders that use advanced generics to build constants with static types. Simplified example (I have much more complex uses which work with advanced optimizations) coming tomorrow. |
One thing I haven't been able to figure out is when we see code like
We can evaluate |
I'm not coming up with anything off-hand. To make the issue clear, let me propose another example: class Bar {
foo: string;
constructor(foo: string) { this.foo = foo;}
}
type Mapped<T> = {[K in keyof T]: number};
type Baz = Mapped<Bar>;
let baz: Baz = { foo: 42 }; // {foo: 'x'} would be an error The only thing the types Bar and Baz share is the keys - but the properties themselves are unrelated (one must be a string, one must be a number). It could be possible to force a type mismatch between these types when we detect that a property key is shared between them, but I'm wary of doing this too much as it will cause lots of backoff - it basically invalidates that entire property name on all types IIUC, but really we only want to link the property name on two types. There are some Closure Library primitives in goog.reflect that could possibly help (though I haven't tested it), or maybe |
Mapped types themselves are fundamentally incompatible with Closure Compiler.
However, the results of mapped types (once all type parameters have been specified) are normal object types that can work fine.
Currently, mapped types are always emitted as
?
.This FR is to emit any mapped type whose properties are known to Tsickle (IOW, if the
in
clause is a resolved type, not a type parameter) as a record literal type ({foo: !Type, ...}
).This gets interesting with named mapped types (
type Foo = { [name in ...]: .... }
); where possible, we should reference the name for brevity / clarity / (for classes or interfaces) to avoid property invalidations.There are (in general) three categories of type references:
{}
, not hidden behind an alias)?
if not expressibleNote that these guidelines apply to all type features that are not supported by JSCompiler but can produce object types; this also includes intersection types (#886) & conditional types.
Note that computing whether a type is expressible is a recursive operation that depends on the type parameters passed (examples coming tomorrow).
The text was updated successfully, but these errors were encountered: