microsoft / microsoft/TypeScript

[Proposal] Type assertion statement (type cast) at block-scope level

Open
#10,421 89 comments 147 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Revisit Suggestion
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

This is a proposal in order to simplify the way we have to deal with type guards in TypeScript in order to enforce the type inference.

The use case is the following. Let us assume we have dozens (and dozens) of interfaces as the following:

Code

interface AARect {
    x: number; // top left corner
    y: number; // top left corner
    width: number;
    height: number;
}

interface AABox {
    x: number; // center
    y: number; // center
    halfDimX: number;
    halfDimY: number;
}

interface Circle {
    x: number; // center
    y: number; // center
    radius: number;
}

// And much more...

And we have a union type like this one:

type Geometry = AARect | AABox | Circle | // ... And much more

It is quite easy to discriminate a type from another with hasOwnProperty or the in keyword:

function processGeometry(obj: Geometry): void {
    if ("width" in obj) {
        let width = (obj as AARect).width;
        // ...
    }
    if ("halfDimX" in obj) {
        let halfDimX = (obj as AABox).halfDimX;
        // ...
    }
    else if ("radius" in obj) {
        let radius = (obj as Circle).radius;
        // ...
    }
    // And much more...
}

But, as we can see, this is quite burdensome when we need to manipulate obj inside each if block, since we need to type cast each time we use obj.

A first way to mitigate this issue would be to create an helper variable like this:

    if ("width" in obj) {
        let helpObj = obj as AARect;
        let width = helpObj.width;
        // ...
    }

But this is not really satisfying since it creates an artefact we will find in the emitted JavaScript file, which is here just for the sake of the type inference.

So another solution could be to use user-defined type guard functions:

function isAARect(obj: Geometry): obj is AARect {
    return "width" in obj;
}

function isAABox(obj: Geometry): obj is AABox {
    return "halfDimX" in obj;
}

function isCircle(obj: Geometry): obj is Circle {
    return "radius" in obj;
}

// And much more...

function processGeometry(obj: Geometry): void {
    if (isAARect(obj)) {
        let width = obj.width;
        // ...
    }
    if (isAABox(obj)) {
        let halfDimX = obj.halfDimX;
        // ...
    }
    else if (isCircle(obj)) {
        let radius = obj.radius;
        // ...
    }
    // And much more...
}

But again, I find this solution not really satisfying since it still creates persistent helpers functions just for the sake of the type inference and can be overkill for situations when we do not often need to perform type guards.

So, my proposal is to introduce a new syntax in order to force the type of an identifier at a block-scope level.

function processGeometry(obj: Geometry): void {
    if ("width" in obj) {
        assume obj is AARect;
        let width = obj.width;
        // ...
    }
    if ("halfDimX" in obj) {
        assume obj is AABox;
        let halfDimX = obj.halfDimX;
        // ...
    }
    else if ("radius" in obj) {
        assume obj is Circle;
        let radius = obj.radius;
        // ...
    }
    // And much more...
}

Above, the syntax assume <identifier> is <type> gives the information to the type inference that inside the block, following this annotation, <identifier> has to be considered as <type>. No need to type cast any more. Such a way has the advantage over the previous techniques not to generate any code in the emitted JavaScript. And in my opinion, it is less tedious than creating dedicated helper functions.

This syntax can be simplified or changed. For instance we could just have :
<identifier> is <obj>
without a new keyword assume, but I am unsure this would be compliant with the current grammar and design goals of the TypeScript team.
Nevertheless, whatever any welcomed optimization, I think the general idea is relevant for making TypeScript clearer, less verbose in the source code and in the final JavaScript, and less tedious to write when we have to deal with union types.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No files, tests, or implementation entry points are named. Start by reviewing the proposed assume <identifier> is <type> syntax and its examples, then determine how it would affect TypeScript grammar and type inference. Done means the syntax and its behavior are agreed and implemented with coverage for the shown union-type scenarios.

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
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.