Description
TypeScript Version: 3.5.1, 3.7.2
Search Terms:
Force TS to alias type, optional property, type expand
Code
type Identity<T> = T;
type PleaseDoNotExpand<T> =
| T
| { x : T }
;
type MappedType<ObjT> =
Identity<
& {
readonly [k in Exclude<keyof ObjT, "x">]: (
PleaseDoNotExpand<ObjT[k]>
)
}
& {
readonly [k in Extract<keyof ObjT, "x">]?: (
PleaseDoNotExpand<ObjT[k]>
)
}
>
;
declare function mappedType<ObjT>(obj: ObjT): MappedType<ObjT>;
/*
Expected:
const x: {
readonly y: PleaseDoNotExpand<string>;
readonly z: PleaseDoNotExpand<boolean>;
} & {
readonly x?: PleaseDoNotExpand<number>|undefined;
}
*/
/*
Actual:
const x: {
readonly y: PleaseDoNotExpand<string>;
readonly z: PleaseDoNotExpand<boolean>;
} & {
readonly x?: number | {
x: number;
} | undefined;
}
*/
const x = mappedType({
x: 1,
y: "hi",
z: true,
});
Expected behavior:
Expected x
to resolve to,
const x: {
readonly y: PleaseDoNotExpand<string>;
readonly z: PleaseDoNotExpand<boolean>;
} & {
readonly x?: PleaseDoNotExpand<number>|undefined;
}
Actual behavior:
Actually resolves to,
const x: {
readonly y: PleaseDoNotExpand<string>;
readonly z: PleaseDoNotExpand<boolean>;
} & {
readonly x?: number | {
x: number;
} | undefined;
}
This behaviour is caused by the ?
optional property modifier. It seems to do something that messes up emit, and makes it inconsistent with required properties.
Since required properties do not expand the PleaseDoNotExpand<>
type,
optional properties should also not expand the PleaseDoNotExpand<>
type.
I believe this is a bug in emit.
Playground Link:
Related Issues:
Kind of related to this,
#34556
That issue wants a way to future-proof ways of forcing TS to not alias a type.
The Identity<>
trick works nicely at the moment.
Also related to this,
#34777
That issue wants a way to force TS to always alias a type.
There is a workaround using interface
when the properties are always known and you do not have union types.
It does not apply in this case, because I have union types.
This mini-rant probably belongs in a different issue but...
I would like a way to have TS never expand a type alias for hover tooltip and emit.
Not by default, anyway.
My actual PleaseDoNotExpand<>
type is easily 100+ lines long. It makes everything unreadable, when TS fully expands it.
I can read PleaseDoNotExpand<Date>
better than I can read the following,
readonly createdAt?: Date | tsql.IExpr<{
mapper: Mapper<unknown, Date>;
usedRef: tsql.IUsedRef<{}>;
}> | (tsql.IQueryBase<{
fromClause: tsql.IFromClause<tsql.FromClauseData>;
selectClause: [tsql.IColumn<{
tableAlias: string;
columnAlias: string;
mapper: Mapper<unknown, Date>;
}> | tsql.IExprSelectItem<{
mapper: Mapper<unknown, Date>;
tableAlias: string;
alias: string;
usedRef: tsql.IUsedRef<never>;
}>];
limitClause: tsql.LimitClause | undefined;
compoundQueryClause: readonly tsql.CompoundQuery[] | undefined;
compoundQueryLimitClause: tsql.LimitClause | undefined;
mapDelegate: tsql.MapDelegate<never, never, unknown> | undefined;
}> & tsql.IQueryBase<{
fromClause: tsql.IFromClause<{
outerQueryJoins: readonly tsql.IJoin<tsql.JoinData>[] | undefined;
currentJoins: undefined;
}>;
selectClause: readonly (tsql.ColumnMap | tsql.IColumn<tsql.ColumnData> | tsql.IExprSelectItem<tsql.ExprSelectItemData> | tsql.ColumnRef)[] | undefined;
limitClause: tsql.LimitClause | undefined;
compoundQueryClause: undefined;
compoundQueryLimitClause: tsql.LimitClause | undefined;
mapDelegate: tsql.MapDelegate<never, never, unknown> | undefined;
}> & tsql.IQueryBase<{
fromClause: tsql.IFromClause<{
outerQueryJoins ...
Activity
AnyhowStep commentedon Dec 10, 2019
Smaller repro,
Playground
So, it might not be the
?
modifier.It's probably because I'm union'ing
PleaseDoNotExpand<T>
with a type that is not a subtype ofPleaseDoNotExpand<T>
Playground
AnyhowStep commentedon Dec 10, 2019
So... I guess this is a feature request?
Suggestion
I would like the opposite of #34556 , which asks for a way to force TS to always expand a type.
I would like a way to force TS to never expand a type.
Use Cases
More readable emit! For declaration files, for tooltip hover, for error messages, etc.
At some point, with complex projects, emit for certain types end up being hundreds of lines long.
Either because of large objects, or large unions, or large intersections, etc.
Some of these large types actually have very short and intuitive names (their type alias identifier).
But TS seems to always expand the type once you put it in a union,
and we lose the readable type.
Right now, there is a general workaround for always expanding a type. (The
Identity<>
trick).However, there is no general workaround for never expanding a type.
There is a special case described here: #34777 (comment)
Examples
Look at how unreadable the expanded type is for tooltip hover (and also .d.ts emit) =(
Playground
Look at how unreadable the expanded type is for error messages =(
Playground
Checklist
My suggestion meets these guidelines:
[-]Optional property modifier causes emit bug; overly eager type expansion[/-][+]Future-proof always-aliasing of mapped/intersection/union/etc. types[/+]AnyhowStep commentedon Dec 10, 2019
I guess I want the kind of aliasing behavior that
interface
andclass
have, since their identifiers are always used, but fortype
.I wonder if the following syntax would make any sense at all,
In all aspects, the above is a type alias, except for the fact that its type will not expand in emit
AnyhowStep commentedon Dec 12, 2019
Here's a workaround (for my specific case),
Playground
The idea is to introduce a new type alias that is a union of
undefined
and the type I originally had.The emit isn't as nice as I would like it to be, but so much better than before.
AnyhowStep commentedon Dec 12, 2019
The workaround outlined above does not always work.
It works for the tooltip,

It does not work for

.d.ts
files,But it seems to only break for
boolean
as a type parameter.Is this a bug?
RyanCavanaugh commentedon Dec 12, 2019
@AnyhowStep I can't work with reports this big. Please, please, please post something succinct once you've boiled it down to something actionable.
AnyhowStep commentedon Dec 12, 2019
The original comment is a repro of the problem for my use case, which is pretty small, imo.
The second comment is an even smaller repro. Writing the original comment helped me realize there might be a different thing causing problems for me. I experimented and realized that it was unions (not optional properties) that were removing the type alias from emit. Optional properties just happen to always union with
undefined
.The third comment is me realizing this might be better as a feature request, since unions seem to have exhibited this behavior forever. However, such behaviour is undesirable for optional properties (in my opinion).
The fourth comment is a half-hearted proposal, mostly thinking out loud.
The last comment is a workaround for my specific use case, which might be useful to others who encounter this problem, in the future. (And also screenshots to show that the workaround doesn't always work)
Which parts are irrelevant? Which parts should I cull? Which parts do I keep quietly to myself?
I've noticed that you hate it a lot when I make chains of comments. But, to me, these comments help me think, and serve as a guide to others (and future me) who may run into these exact problems some time in the future.
I don't like it when I see threads where people just post stuff without explaining what their thought process is. So, I try to provide my thought process, where I can.
I just gave it some thought.
If I were to create an entirely new issue for this feature request, and rearrange my comments here into the template, I would basically still have the content of all five comments... Just combined into one massive post.
Would that be better?
RyanCavanaugh commentedon Dec 12, 2019
The issue tracker is for bug reports or feature requests. I can assign these units of work to people and determine whether or not they're complete.
I can't take a stream of conscious blog about your thoughts on a topic and (in any reasonable length of time) determine what needs to be done, whether it's a bug or not, and schedule it. I can't even tell when the stream of comments is going to stop, so it feels constantly premature to try to do anything with your issues.
The vast, vast majority of things we deal with here can be reasonably described in 1 or 2 screenfulls of text. If you think you need 8 pages of text to define something, it's probably not something that's in a state for us to be making decisions on. It seems like you need to workshop these things in some other forum first until you have a more concise way of communicating what you'd like to happen. Does that make sense?
AnyhowStep commentedon Dec 12, 2019
I opened a new issue that is shorter