There are a handful of issues that are both separate features, but are also loosely related in that they speak to certain problematic aspects of generics today. In particular,
- #7061 (specifically later in the thread) asks for a way to assign a private alias to a complex typed derived
from the actual type parameters, usable from within the class/function signature. There are suggestions to use
namespaces and other hacks, along with explanations of why
<T, U extends Foo<T> = Foo<T>>
is insufficient. - #26242 and #16597 both pertain to partial inference of type parameters. The rough summary is that you
can write
<T, U = Foo>
, but there's no way to explicitly specifyT
and still get inference onU
. If you omit the<>
entirely, then bothT
andU
are inferred and the= Foo
initializer is unused. If you specify a<T>
, thenU
is initialized directly from the= Foo
initializer without any inference happening. There are many dimensions to this question, including where exactly the inference should be opted into. - #40468 and #23689 pertain to some way to do additional validation of type parameters, beyond what a simple lower bound can achieve, and to give understandable/actionable error messages when those constraints are violated.
In recent versions, we've seen a handful of new keywords making their way into type parameter lists: <in T>
and
<out T>
as explicit variance annotations for classes/interfaces, and a proposed new <const T>
to hint to the
inferencer that the most specific (cf. as const
) possible type should be used. I propose a few more additions that
would address the above issues. They could be taken separately, but also work in synergy.
This would solve #7061 by allowing a declaration to declare a handful of private type parameters. The linked
issue notes that there are syntactic concerns with trying to specify these types anywhere else (i.e. some proposals
to add an extra block somewhere between the declared name and the definition/body, but this is awkward and would be
necessarily different for functions vs. classes, at least). There are also concerns with whether these types would
be exported as general "associated types" accessible from outside the class definition (or function signature+body),
which starts getting into generic namespaces (#19728). The use of private
neatly avoids this concern by making
it pretty clear that these type aliases cannot be accessed outside the class/function.
function makeTable<
R, C, V,
private Table = Map<R, Map<C, V>>
>(rows: R[], cols: C[], init: (r: R, c: C) => V): Table {
// ... (type `Table` available in scope here, too)
}
See also #18074 for some slightly better motivated examples.
This is almost workable today, in that you can write this without the private
keyword
and it works as long as (1) you specify R
, C
, and V
explicitly, (2) you don't pass
a fourth parameter, and (3) you don't need strict type equality. If you omit the explicit
types, then (today, without private
) Table
is inferred rather than initialized with the
specified initializer, and thus the following bad code compiles:
const table: Map<string, Map<string, 42>> = makeTable(['x'], ['y'], () => 0);
This is surprising, since table.get('x')?.get('y')
is actually 0
, but the type checker
thinks it's 42
. This happened because the inferencer inferred a subtype for Table
, rather
than the specified type. A caller could also explicitly specify whatever type they wanted
(again, subject to any lower bound), for a similar effect. The issue about strict type equality
shows up if Table
were used in a contravariant context, since the type checker can only
guarantee that Table
is a subtype of the specified lower bound: this makes it impossible to
assign a Map<R, Map<C, V>>
to a variable of type Table
, since the type checker understands
the first half of this paragraph and thus knows that it could require any subtype.
A more robust workaround is to (1) add an impossible to satisfy type parameter and check that
it was not specified as never
or any
, or (2) wrap the aliased type in an invariant
wrapper so that subtypes can't be inferred/specified. But these are awkward, inefficient,
and can make inference more brittle.
- Private type parameters must occur at the end of the parameter list: no non-private parameter may follow a private parameter.
- Private type parameters must be initialized (i.e. as
private Foo = ...
). This fits well with them apearing after all non-privates, since non-privates can include initialized parameters, so a non-initialized private would be a problem. - Callers may not specify an explicit type for the parameter, and no inference occurs for them: they are an exact type alias available only within the definition (i.e. function signature/body, class body and methods, etc).
This speaks to #26242 and/or #16597, which look at two sides of the partial inference question. In particular, there are two options for who requests the partial inference: (1) the declaration, or (2) the caller.
Aside: This is analogous to the two styles of variance commonly seen, e.g. between Java and Kotlin. Java uses call-site variance, such that covariant types like
Future<T>
show up asFuture<? extends T>
in nearly every signature, while contravariant types likePredicate<T>
show up asPredicate<? super T>
everywhere. This allows finer-grained control and flexibility (e.g. when an invariant type is being used covariantly), but is also very easy to get wrong. Kotlin fixed this by introducing declaration-site variance: allowing classes to be annotated as<in T>
or<out T>
, which was the inspiration for the same feature in TypeScript (though unlike Java, TypeScript already had reasonably good, if inefficient and harder-to-debug, inference for this).
We could easily support both/either declaration-site and/or call-site inference requests. Like the variance aside, call-site allows more usage flexibility, but does not empower library authors to make safe/expressive APIs as easily.
For the declaration-site version:
function makeGraph<V, N = infer>(edges: Map<N, N[]>): Graph<N, V> { ... }
const g = makeGraph<number>(new Map([[1, [1, 2]], [2, [1]]]));
In this case, we consider a Graph
type where elements can have an optional annotation of type V
. Without
the partial inference, one would have to write one of
const g1: Graph<N, V> = makeGraph(...);
const g2 = makeGraph<V, N>(...);
both of which require duplicating the (potentially complex) N
type, or else if N
is unspecified then it's
initialized to whatever default (possibly just unknown
) independent of the already-known type of the map passed
to it.
A more compelling example comes up in testing: an expected-type test might want to write
type CheckSame<A, B> = A extends B ? B extends A ? [] : [never] : [never];
function expectType<T, private U = infer>(_arg: U, ...rest: CheckSame<T, U>): void {}
expectType<Promise<number>>(Promise.resolve('x').then(() => 42));
Without the partial inference, the above is impossible to write without an extra artificial curry added to the
call (i.e. expectType<Promise<number>>()(Promise.resolve('x').then(() => 42));
), which shows that TS is capable
of doing the inference, but it forces an unacceptably awkward API.
Note that the private
helps prevent misuse (and note also that other "type alias" solutions to the private
issue above would not work so well with infer
). The [never]
approach could further be ameliorated by a later
proposal in this document.
Use-site inference could fill in any gaps where an API author didn't specify infer
, but it's desired anyway.
In this case (if makeGraph
weren't written with infer
), one might write
declare const [a, b]: SomeComplexTypeWeDontWantToWriteAgain[];
const map = makeGraph<string, infer>([[a, [a, b]], [b, [a]]]);
- This only applies to functions/methods: I believe an inferred type in a class or interface is probably nonsense.
- If a type parameter is declared as (or initialized to)
infer
then the type inferences will use inference to initialize it. The use of<T = infer>
rather than (say) a prefix<infer T>
makes sense because it occurs under exactly the same circumstances as the use of a default initializer today. (I also don't believe it makes sense to combine them:infer T = number
is probably nonsense). - Inferred type parameters may appear anywhere an optional/initialized parameter can appear.
- When a parameter is
private T = infer
, then inference is mandatory: the caller cannot override it. Otherwise, inference only occurs when an explicit type is not specified in the parameter list.
- Use-site inference could also work by (1) leaving a hole in the type parameter list (
foo<, number>()
) or (2) using some other placeholder (_
,*
, etc); or could be left out entirely. - What about inferring the first parameter in a template type list? e.g. for
function foo<T = infer, U = number>()
, if you call it asfoo()
then bothT
andU
get inferred (rather thanU
being initialized tonumber
), butfoo<>()
is currently invalid syntax. - What about
infer
in a constructor? The class-level type parameter isn'tinfer
, but how to express it on a constructor? - Is it even workable to use
infer
(or any other valid identifier, such as_
, which has been suggested for the call-site placeholder) here? What ifinfer
is already defined in scope as a type name? Do we need a non-identifier syntax (i.e.= *
) to support this instead? (This would slightly degrade the default initializer analogy but wouldn't change the semantics)
#40468 and #23689 ask for some way to get more direct error messages out of various conditions in the type checker.
If I want to make a method that requires passing an enum container, I might write something like
type Guard<T> = (arg: unknown) => arg is T;
type EnumElement<T> = T extends EnumOf<infer E> ? E : error `${T} is not an enum`;
declare function enumMemberPredicate<E>(enum: E): Guard<EnumElement<E>>;
where the details of EnumOf<T>
would require a more complicated usage than what's seen here (and is thus
out of scope). But the gist remains that we can do some type-level validation and emit a clear error if a
given branch of a conditional type is taken.
I'm less clear on the exact semantics here. I think the error should be emitted if the type ever shows up outside of a type definition or conditional. So the following would not emit an error:
type Foo<T> = error `foo: ${T}`;
type Okay = number extends string ? error `wat` : boolean;
In the case an error is emitted, the resulting type is indistinguishable from any
. I don't believe it's
a good idea to allow any sort of recovery or matching on error types, so one could not write
type Caught<T> = T extends error (infer E) ? E : never;
This is somewhat of a more limited version of error
. Rather than allowing arbitrary errors, it would go
a long way to simply provide a weaker version of the extends
keyword in type parameter lists, which
specifically would not establish an actual lower bound. This would thus allow a cyclic reference to the
left-hand side in the expression on the right.
type Guard<T> = (arg: unknown) => arg is T;
type Literal<T> =
[T] extends [number] ? (number extends T ? never : T) :
[T] extends [string] ? (string extends T ? never : T) :
[T] extends [boolean] ? (boolean extends T ? never : T);
function literalGuard<T satisfies Literal<T>>(arg: T): Guard<T> {
return (actual) => actual === arg;
}
There are numerous workarounds to get a reasonable type error if a literal is not passed. For instance:
declare function literalGuard1<T>(arg: T extends Literal<T> ? T : never): Guard<T>;
declare function literalGuard2<T>(arg: T, ...rest: T extends Literal<T> ? [] : [never]): Guard<T>;
declare function literalGuard3<T>(arg: T): T extends Literal<T> ? Guard<T> : 'Whoops, pass a literal';
but these do not provide reasonable error messages, and/or fail much later, probably far away from the
actual problemmatic code. Replacing satisfies
with extends
in the example is an error because it
results in a circular reference (since extends
establishes a lower bound to be used as a placeholder
for the free type variable).
Note that this can play quite nicely with infer
and private
above.
satisfies
would perform the same bounds checking (and type errors) asextends
, but it would not establish a lower bound or make any further guarantees about the type itself.
- Should the behavior be the same as
extends
(i.e. LHS must be a subtype of RHS) or does it make sense to look for a keyword that better expresses the idea of a predicate? I.e. we could have a type that returnstrue
orfalse
types instead. While having the type itself specifically be a subclass isn't actually helpful since there's no actual lower bound, I believe it's still the best option since the conditional aspect of it is very well-defined.