microsoft / microsoft/TypeScript
(Proposal) more-sophisticated CFA, and Narrowing Boolean Types
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
Description
### 🔍 Search Terms
more advanced sophisticated CFA control flow analysis inferred working across assignments variables `const`s spreads narrowing constraining types
ambient external term narrowing constraining is predicate asserts assertion boolean type types
discriminant discriminated union intersection type types literal field prop property fields props properties
### ✅ Viability Checklist
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- [x] This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
### ⭐ Suggestion
**A demand for more-sophisticated CFA, working across indirections, eg variables, assignments, and spreads (see below).**
**Sophisticated boolean types ` is T` (Type-Predicate(ing) Boolean Type(s) ) to be inferred in, and permitted as, signatures of boolean variables eg `const fetching: /* boolean */ c is FetchAction`.**
### 📃 Motivating Example
```typescript
interface Action {
readonly cls: string & { __c?: any }
}
/** 'Create, Read, Update, Delete'. */
interface CrudAction extends Action {
}
interface GlobalRefreshAction extends CrudAction {
readonly cls: "refresh"
}
interface FetchAction extends CrudAction {
readonly cls: "fetch"
readonly query: string
}
interface UpdateAction extends CrudAction {
readonly cls: "update"
readonly target: string
readonly newValue: Value
}
```
```typescript
function dispatch(c: /* Action | */ GlobalRefreshAction | FetchAction | UpdateAction ): void
{
const fetching = c.cls === "fetch"
const updating = c.cls === "update"
{ /* directly on `c` */
if (fetching) {
c.cls /* "fetch" */
} else {
c.cls /* "refresh" | "update" */
}
}
{ /* on `cStats` */
const cStats = { ...c } as const
/* this already works today */
// if (cStats.cls === "fetch" ) {
// cStats.cls
// }
/* this doesn't currently work */
if (fetching) {
cStats.cls /* still "fetch" | "refresh" | "update" 😒 */
} else {
cStats.cls /* still "fetch" | "refresh" | "update" 😒 */
}
}
...
}
```
Currently, narrowing/constraints doesn't extend across spreads, as shown above, where `cStats.cls` remains `"refresh" | "fetch" | "update"` in either branch.
CFA should ideally, additionally keep track of spreads (and calls) at play. checking on `fetching` should ideally narrow `cStats` to `{ readonly cls: "fetch", readonly query: string }`, given that `c` in `const cStats = { ...c } as const` above narrow to `FetchAction`.
Allow sophisticated boolean types ` is T` to be inferred in, and permitted as, signatures of boolean variables eg `const fetching: /* boolean */ c is FetchAction`. This provides a (necessary) level of type-safety missing so far. This also strengthens basis for more-sophisticated CFA actualisations.
```typescript
/* inferred types */
const fetching /* : c is FetchAction */ = c.cls === "fetch"
const updating /* : c is UpdateAction */ = c.cls === "update"
```
```typescript
/* signatures */
const fetching: /* boolean */ c is FetchAction
const updating: /* boolean */ c is UpdateAction
```
```typescript
/* declaration emit */
interface ActionStats {
readonly c: Action;
readonly fetching: c is FetchAction;
readonly updating: c is UpdateAction;
}
```
```typescript
/* declaration emit */
function getCrudActionStats(c: CrudAction | GlobalRefreshAction | FetchAction | UpdateAction ): {
readonly fetching: c is FetchAction;
readonly updating: c is UpdateAction;
}
```
Allow them to name terms defined outside the function they're written in (#43368, #43786).
```typescript
function isBrowser(): global is Window;
function isWorker(): global is WorkerGlobalScope;
```
```typescript
/* declaration emit */
interface PlatformStats {
readonly browser: global is Window;
readonly workerScope: global is WorkerGlobalScope;
}
```
Allow operations between them - for example, `global is Window || global is NodeGlobalScope` (`stdin` not available (with)in Worker(s)), `(lhs: B, rhs: C) => lhs is G && rhs is H`.
```typescript
/* declaration emit */
/** the service will only be available in select host engines. it's unavailable in, for example, AudioWorklet, PaintWorklet, ServiceWorker, Deno, wherein `available` will return `false`. */
interface PlatformStats {
readonly available: global is Window || global is WorkerGlobalScope || global is NodeGlobalScope;
}
```
```typescript
/* declaration emit */
function m1(lhs: B, rhs: C): lhs is G && rhs is H;
```
more sophisticated examples of binary ops between those types
```typescript
/* declaration emit */
function m2(lhs: B, rhs: D, options: F): (lhs is G && rhs is H ) || lhs is K || (lhs is L && rhs is M) ;
```
```typescript
/* declaration emit */function m3(lhs: B, rhs: D, options: F): (lhs is G || rhs is H ) && (lhs is L || rhs is M) ;
```
Allow their usage in conditional types; allow conditional types to operate directly on preceding parameter terms (`otherParam is M ? T1 : T2`).
```typescript
// function perform(stream: S, c: S extends Writer ? (WriteAction | ReadAction) : ReadAction ): ActionResult
function perform(stream: Stream, c: stream is Writer ? (WriteAction | ReadAction) : ReadAction ): ActionResult
function perform(writes: Boolean, c: writes is true ? (UpdateAction | PruningAction) : (FetchAction | RefreshAction) ): ActionResult
```
- additionally, in case of `otherParam` conforming to `boolean` (or `Boolean`), allow simply writing `otherParam ?` in-place.
```typescript
function perform(writes: boolean, c: writes ? (UpdateAction | PruningAction) : (FetchAction | RefreshAction) ): ActionResult
```
Allow `extends` clause in `interface`s to name union types in place of known-name object types (#202). This avoids the self-dealiasing nature of type-aliases (eg `type T = ...`, `type T = T1 | T2 | ...`), instead acting as nominal types, preserved across assignments, calls and inference (`interface`s are generally inferred as-is, not dealiased).
```typescript
interface CommonAction extends (GlobalRefreshAction | FetchAction | UpdateAction) { }
```
### 💻 Use Cases
### Use-cases
A series of `const ... = ...`s and conditionals.
Working with JSX-based frameworks.
Writing UI components.
Complex logic involving UT(s) (Discriminated Union Type(s)) and conditionals. Some sophisticated inner logic.
Other use-cases with Declaration Emit (DTS Emit; `.d.ts`) at play.
### `interface Action` and `function dispatch` example
```typescript
interface Action {
readonly cls: string & { __c?: any }
}
interface GlobalRefreshAction extends Action {
readonly cls: "refresh"
}
interface FetchAction extends Action {
readonly cls: "fetch"
readonly query: string
}
interface UpdateAction extends Action {
readonly cls: "update"
readonly target: string
readonly newValue: Value
}
// interface CommonAction extends (GlobalRefreshAction | FetchAction | UpdateAction) { }
// function supportsCommonAction(c: CommonAction ): boolean ;
function dispatch(c: /* Action | */ GlobalRefreshAction | FetchAction | UpdateAction )
: void
{
const fetching = c.cls === "fetch"
const updating = c.cls === "update"
{
if (fetching) {
c.cls
} else {
c.cls
}
if (fetching || updating) {
c.cls
} else {
c.cls
}
}
{
const cStats = { ...c } as const
if (cStats.cls === "fetch" ) { cStats.cls }
// if (cStats.cls === "fetch" ) {
// cStats.cls
// } else {
// cStats.cls
// }
if (fetching) {
cStats.cls
} else {
cStats.cls
}
if (fetching || updating) {
cStats.cls
} else {
cStats.cls
}
}
{
const cStats = !fetching ? ({ ...c } as const) : ({ ...c } as const)
// if (cStats.cls === "fetch" ) {
// cStats.cls
// } else {
// cStats.cls
// }
if (fetching) {
cStats.cls
} else {
cStats.cls
}
if (fetching || updating) {
cStats.cls
} else {
cStats.cls
}
}
}
```
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No files, tests, or compiler entry points are named. Start by separating the motivating examples into independently scoped CFA, boolean type-predicate, conditional-type, and interface-extension requirements; done would require an agreed scope plus implementation and regression coverage for the selected cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100