Conditional Distributions Allow Constraint Violation
#63708
type Issue<A, B extends A> =
A extends unknown ?
B extends unknown ?
Show<A, B> :
never :
never;
type Show<A, B> = [A, B] & {};
type X = Issue<0 | 1, 0 | 1>;
// ^?
-
The problem is that when we distribute on A and B, then we should create a new type parameter for each one with an identical name.
-
So we are prototyping this.
-
This is a little complicated by the fact that this now also becomes a declaration site rather than just a usage.
- We would need to introduce something in the binder for this.
- So that would say "this is a distribution site with a new type variable".
-
But that would have broader implications beyond just type variables on the left side of extends.
-
Doing something like this would changing the semantics of type distribution because we'd now operate over any identifier-named type.
-
When explaining to others, we've explained this as "there's a new type variable that is created for distribution", so surprised it didn't work this way.
- Really there's a mix of substitution types and other instantiation mechanics at play here.
-
Do we need substitution types?
- Yes. You need "learned information"/"conditional narrowing" to be tracked along to satisfy other constraints in true branches.
- Wait really? Why can't
T extends number become T' extends T & number?
-
How does this work over type references that alias bare identifiers
type Foo<T> = T;
type Blah<T> = Foo<T> extends any ? { b: Foo<T> } : undefined;
type What = Blah<1 | 0>;
- Today, this does distribute; it no longer would with the suggested change...
- Oh... that's probably very breaky!
- Who does this?
-
Feels like people should at least have a way to distribute explicitly since we might be changing things here?
-
You are binding a new type parameter at the top; there's no reason you couldn't other than efficiency.
- No - we use substitution types to track learned information and conditional narrowing.
- There are differences in type comparisons when you have substitution types, and they differ in how they act on type comparisons.
- So while they feel similar, type parameters with added constraints are treated differently in practice.
T'' extends T' & (1 | 2) extends T & number isn't reasoned about quite the same as T'' & (1 | 2) & number.
-
Why don't we just introduce syntax to avoid breaking people?
- The problem is that we are trying to fix a soundness hole and we want that to be fixed by default.
-
Prototype will tell us what breaks in top 999.
API Bikeshed
import { version, versionMajorMinor } from "typescript";
import * as ts from "typescript/async"; // or sync
import * as ast from "typescript/ast";
- Currently there's a bunch of subpath exports in the
typescript package.
- As we prototyped replacements with the new API, we noticed old API usage was annoying to go through different imports.
- Feedback from early API users is that it's less overwhelming than having one import with everything.
- One of the things we don't like with the "siloing" is that it kind of draws boundaries that we might be fixing ourselves into.
-
Can't really have a single barrel given the sync/async split.
-
If you want a single barrel, you can do it yourself!
// ./src/typescript.ts
// Single barrel export for convenience
export * from "typescript";
export * from "typescript/ast";
export * from "typescript/async";
// package.json
{
// ...
"imports": {
"#ts": "./dist/typescript.js"
}
}
// Other usage
import * as ts from "#ts";
- What about identical types imported through different paths?
- e.g.
ModuleKind comes from both "typescript" and "typescript/async".
- What do people want from this?
- Do the names overlap between sync/async modules?
- Yes, they have identical names.
- It'd be annoying if they all had an
Async postifx.
- Could we just simplify this all into
typescript/async and typescript/sync, and re-export common stuff from both?
- What do you do if you have a function that acts only on the common stuff?
- Any AST walker.
- Aside: do we have
forEachChildAsync?
- Currently a problem because
forEachChild short-circuits on truthy results. Promises are always truthy, so a walk always exits early.
- If we had the FIFO prototype resurrected (come back to this Jake Bailey (@jakebailey)?), would people even want async because the context switching is so high?
- Async is just painful
- But it's the only way to easily do things concurrently.
- Wonder how easy it is to use workers and just use the sync layer.
- Out of time - really need to build some prototypes and get feedback from others.
Conditional Distributions Allow Constraint Violation
#63708
The problem is that when we distribute on
AandB, then we should create a new type parameter for each one with an identical name.So we are prototyping this.
This is a little complicated by the fact that this now also becomes a declaration site rather than just a usage.
But that would have broader implications beyond just type variables on the left side of
extends.Doing something like this would changing the semantics of type distribution because we'd now operate over any identifier-named type.
// TODOWhen explaining to others, we've explained this as "there's a new type variable that is created for distribution", so surprised it didn't work this way.
Do we need substitution types?
T extends numberbecomeT' extends T & number?How does this work over type references that alias bare identifiers
Feels like people should at least have a way to distribute explicitly since we might be changing things here?
You are binding a new type parameter at the top; there's no reason you couldn't other than efficiency.
T'' extends T' & (1 | 2) extends T & numberisn't reasoned about quite the same asT'' & (1 | 2) & number.Why don't we just introduce syntax to avoid breaking people?
Prototype will tell us what breaks in top 999.
API Bikeshed
typescriptpackage.astfactoryisCan't really have a single barrel given the sync/async split.
If you want a single barrel, you can do it yourself!
ModuleKindcomes from both"typescript"and"typescript/async".Asyncpostifx.typescript/asyncandtypescript/sync, and re-export common stuff from both?forEachChildAsync?forEachChildshort-circuits on truthy results.Promises are always truthy, so a walk always exits early.