How can Flow be forced to cast a value to another type?

17,527

Solution 1

Flow doesn't do direct casting from one type to another, but you can do something like

const bar: string = (foo: any);

so you cast foo to an any, because any accepts any type of value as an input. Then because the any type also allows you to read all possible types from it, you can assign the any value to bar because an any is also a string.

Solution 2

In the example you give you're looking at a "cast" from a union type to one of its members. While it's common to think of this as a cast, it's not the same as type-casting in other languages.

By setting the type of foo to string | number, we've told Flow that this value could be either a string or a number. We then happen to put a string in it, but Flow doesn't discard our direct assertion about its type because of that, even in situations (like this one) where it couldn't change later.

To assign it to a string-typed variable, Flow needs to know that even though it might have been either a string or number, by the time we do the assignment we are sure that it can only be a string.

This process of reducing the possible options is called type refinement.

Type refinements

To refine the type, we need to prove that it must be the type we say it is, in a way Flow understands.

In the original example, you could do this using typeof:

type StringOrNumber = string | number
const foo: StringOrNumber = 'hello'

// This would raise an error here:
// const bar: string = foo

if (typeof foo === "string") {
  // Flow now knows that foo must be a string, and allows this.
  const bar: string = foo
}

Not everything that a human can see as a type refinement is understood by Flow, so sometimes you'll need to look at the refinement docs to see what might make Flow understand it.

Suppression comments

Sometimes there's no way to express the safety of a refinement to Flow. We can force Flow to accept a statement through use of a suppression comment, which will suppress an error Flow would otherwise report. The default suppression comment is $FlowFixMe, but it can be configured to a different comment or comments.

Flow will report an error on the second line of this, reporting that unionValue might be of type 'number':

const unionValue: StringOrNumber = 'seven'
const stringValue: string = unionValue

However, by using a suppression comment, this passes Flow:

const unionValue: StringOrNumber = 'seven'
// $FlowFixMe: We can plainly see this is a string!
const stringValue: string = unionValue

One useful feature of suppression comments is that a suppression comment without a following error to suppress is considered an error. If we change the type in the example:

const unionValue: string = 'seven'
// $FlowFixMe: Even though this is a string, suppress it
const stringValue: string = unionValue

Now Flow will report an "Unused suppression" error instead, alerting us. This is particularly useful when Flow should be able to recognize a refinement but can't - by using a suppression comment, we're alerted to remove the comment (and gain additional type-safety) if a future version of Flow recognizes the code as type-safe.

Cast-through-any

If you really can't express it in a way that demonstrates its safety to flow, and you can't (or won't) use a suppression comment, you can cast any type to any, and any to any type:

const unionValue: StringOrNumber = 'seven'
// Flow will be okay with this:
const stringValue: string = (unionValue: any)

By casting a value to any we're asking Flow to forget anything it knows about the type of the value, and assume whatever we're doing with it must be correct. If we later put it into a typed variable, Flow will assume that must be right.

Cautions

It's important to note that both suppression comments and cast-through any are unsafe. They override Flow completely, and will happily perform completely nonsensical "casts":

const notAString: {key: string, key2: number} = {key: 'value', key2: 123}
// This isn't right, but Flow won't complain:
const stringValue: string = (notAString: any)

In this example, stringValue is holding the object from notAString, but Flow is sure that it's a string.

To avoid this, use refinements that Flow understands whenever you can, and avoid use of the other, unsafe "casting" techniques.

Solution 3

This answer is just a suggestion. When browsing around solutions to Event and HTMLElement related type checking issues I encountered a lot of guards invoking instanceof.

To satisfy type checks I just introduced this generic guard and called it cast (which does not of course make it a cast), because otherwise my code got so bloated.

The cost is of course in performance (pretty relevant when writing games, but I guess most use cases benefit more from type guards than milliseconds per iteration).

const cast = (type : any, target : any) => {
    if (!(target instanceof type)) {
        throw new Error(`${target} is not a ${type}`);
    }
    return target;
}

Usages:

const fooLayer = cast(HTMLCanvasElement, document.getElementById("foo-layer"));
window.addEventListener("click", (ev : Event) =>
  console.log(cast(MouseEvent, ev).clientX - cast(HTMLElement, ev.target).offsetLeft,
    cast(MouseEvent, ev).clientY - cast(HTMLElement, ev.target).offsetTop));
Share:
17,527
czerny
Author by

czerny

Updated on June 03, 2022

Comments

  • czerny
    czerny almost 2 years

    Is it possible to forcibly cast a variable in Flow?

    type StringOrNumber = string | number
    const foo: StringOrNumber = 'hello'
    
    // I look for something like `const bar:string = (string) foo`
    const bar: string = foo // fails
    const bar: string = (foo: string) // also fails