[[Early thoughts around conditional types]]
-
Currently have
T extends Foo
as a type operator that returnstrue
orfalse
.- Seems very strange as to what that actually means:
declare function foo<T>(x: T, y: T extends string): void;
- This function takes an
x
, and ifx
is astring
,y
must betrue
or otherwisefalse
. - Not entirely useful.
- Seems very strange as to what that actually means:
-
Key idea: you want to be able to narrow in the true branch of a conditional type.
- Example
type DeepReadonly<T> = T extends any[] ? ReadonlyArray<T[number]> : T extends object ? DeepReadonly<T> : T;
- Need to be able to know that
T
is an array type to actually perform the indexed accessT[number]
-
Could say users need to replace instances of
T
withT & any[]
- Not correct though -
(T & any[])[number]
gives youany
. - If we have
type unknown = {} | null | undefined
and usedunknown[]
might be better?- Eh, gives weird union types.
- Not correct though -
-
Really what you want is to refine the constraint of
T
. -
Idea:
T extends Foo
, whereextends
is a type operator that augments the constraint ofT
.- Kind of like intersecting, more correct and consistent.
- What happens when you stack these together? e.g.
T extends Foo extends Bar
?- Simplifies to
T extends Foo & Bar
- Simplifies to
-
Users could potentially write these types out?
- Sure, but not entirely useful; usually would only come out from narrowing in a conditional type.
-
Instantiation?
- Instantiate LHS, if it's still generic, remove RHS.
-
What about narrowing in an expression context?
function f<T>(x: T) { if (typeof x === "string") { return x; } throw new Error("Gimme string!"); }
- The return type here should be
T extends string
.-
What about when
T
isnumber
? -
If you verify against the RHS at instantiation, you can get
never
! -
Becomes a matter of...
- declaration-site/call-site verification:
// vvvvvvvvvvvvvv function f<T extends string>(x: T) { /*...*/ } f(100); // nope!
- vs use-site verification
// T extends string -> number extends string -> never let x: never = f(100); x.whoops; // nope
-
- In a sense, this is sort of related to type predicates!
- Type predicate annotations guarantee a
true
value if a type matches some type. - This returns a value of a more specific type using information upon instantiation.
- Type predicate annotations guarantee a
- The return type here should be
-
- Example
-
In general, we are trying to decide what is the appropriate behavior for the
extends
syntax.- One option is the "is-assignable" operator.
- The other is the "type parameter constraint augmentation" operator
- We want both in the lanugage.
- Could make it context-depenent?
- Could get rid of the "is-assignable" operator, make it part of conditional types.
- Means it's harder to compose with
And<T, U>
andOr<T, U>
. - But composing doesn't even keep track of the information that's flowed through. e.g.
And<T extends Foo, T extends Bar> ? /*trueBranch*/ : /*falseBranch*/
- We don't have a way of tracking that
T
should be treated asT extends Foo & Bar
intrueBranch
.
- We don't have a way of tracking that
- Means it's harder to compose with
- In general, this might be confusing for users.
- Could get rid of the "is-assignable" operator, make it part of conditional types.