Flow: "This type cannot be coerced to string" in template literals

Created on 15 Nov 2016  Â·  37Comments  Â·  Source: facebook/flow

but it has toString method

$ cat test.js
// @flow
console.log(`${ {toString() { return 'string'  }} }`);

$ flow check
test.js:2
  2: console.log(`${ {toString() { return 'string'  }} }`);
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object literal. This type cannot be coerced to
  2: console.log(`${ {toString() { return 'string'  }} }`);
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ string


Found 1 error

Most helpful comment

Hey guys, why was this issue closed? It doesn't feel like it was resolved in any clear way. I have come across the same issue just now: passing in simple props to a React component like this:

src={`/public/img/posters/${props.poster}`}

Like @theY4Kman https://github.com/facebook/flow/issues/2814#issuecomment-288208880, I too would like to understand why flow would be discouraging this?

All 37 comments

technically everything has a toString method :) i agree that in cases where there's an overridden toString it might make sense to allow this. generally, falling back on Object.prototype.toString and getting [object Object] is not what you expect.

Flow forbids implicit type coercion, except in a few cases where the syntax exists solely for that purpose like String(foo) or +bar.

the workaround here is to convert to a string via String(obj) or by calling .toString(). you could also define a tag function like

function debug(quasis: Array<string>, args: Array<any>) {
  let parts = [quasis[0]];
  for (let i = 1; i < quasis.length; i++) {
    parts.push(String(args[i - 1]), quasis[i]);
  }
  console.log(parts.join(""));
}

debug `${foo}`;

but according to this: https://ponyfoo.com/articles/template-literals-strictly-better-strings template literals should generally be used instead of strings

which gives me lots of noise in flow because of this issue

am I getting something wrong or might this issue be worth reconsidering?

I would argue that in the case of template literals, you really do want .toString() to be called on the object irrespective of its implementation. This wouldn't hold for tagged template literals, however.

Yeah, faced the same.

51: axios.delete(/api/posts/${postId})
^^^^^^ Number. This type cannot be coerced to
51: axios.delete(/api/posts/${postId})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ string

This are templates designed for, isn't it? Shouldn't flow bypass this case (ok, at least warning, not error)?

For now, fixed:

axios.delete(/api/posts/${postId.toString()})

@matrunchyk there is no good reason to use Number, so Flow is actually helpful here

@vkurchatkin Why? In that case how to substitute numeric values in strings (e.g. for my particular case)? I don't think concatenation with + is a better choice.

Flow allows number (primitive, not object wrapper)

The precise type isn't the point though - in a context where the code is intentionally performing string concatenation, it seems reasonable to allow .toString() to be called implicitly rather than explicitly.

In javascript everything can be coerced to a string or a number (except for symbols). One of the goals of Flow is to limit this behavior.

@vkurchatkin How would you write that string in the example to pass Flow and ESlint?

@matrunchyk I wouldn't use Number in the first place. Otherwise, calling toString is an option

@vkurchatkin

Flow is to limit this behavior.

In the mean time the doc says:

Generally, implicit type casting is an error with Flow. However, it is a fairly common JavaScript idiom to produce a string by combining a string and a number with the binary operator +, so Flow accepts it.

So it's not allowed to coerce in template strings (which is generally designed to concatenate heterohenous content in one string in nice manner), but it's allowed to concatenate string and number without explicit coercion (which is one of the most error-prone places). This two statements sounds nonsence in sum.

So it's not allowed to coerce in template strings

It is allowed

I don't think I'm understanding this correctly. Lemme try to get this straight...

// (GOOD) implicit concatenation between string and number primitive
console.log(`This is the number twelve: ${12}`);

// (GOOD) concatenation of string and Number wrapper explicitly case to string
console.log(`This is the number twelve: ${new Number(12).toString()}`);

// (BAD) implicit concatenation of string and Number wrapper
console.log(`This is the number twelve: ${new Number(12)}`);

These three examples all print the same thing, but the last one draws an error from flow.

To understand why this is so, I've read and reread the explanations in this thread with a fine-toothed comb, and I've learned the reason why implicit concatenation of strings and Number wrappers is disallowed is because it's Very Bad™ and on the flow Disapproval List®.

Sarcasm aside, there's a reason why implicit string concatenation with Number wrappers is disallowed. Why is this? If the reason is because "an aim of flow is to limit this usage", why is this usage discouraged? What are the potential errors we are trying to avoid?

Irrespective of whether Flow's behaviour is changed, a better message to the developer would be appreciated. React is good in this regard, giving you pointers on why there was a warning or error.

Hey guys, why was this issue closed? It doesn't feel like it was resolved in any clear way. I have come across the same issue just now: passing in simple props to a React component like this:

src={`/public/img/posters/${props.poster}`}

Like @theY4Kman https://github.com/facebook/flow/issues/2814#issuecomment-288208880, I too would like to understand why flow would be discouraging this?

For numbers I've "fixed" it by prefixing with +:

`...${n}...` // KO
`...${+n}...` // OK

You don't need workarounds for number, they just work

Well indeed my use-case was more involved but I have simplified my sample too much 😄 :

`...${myMapOfNumbers.get(someKey)}...` // KO
`...${+myMapOfNumbers.get(someKey)}...` // OK

The map was typed as Map<string, number>.

@mroch okay so is there a way to explicitly annotate a certain type that we've defined an appropriate toString for, so that flow allows us to coerce it to a string? It's too convenient not to want to use in some cases.

Keep in mind, this is basically preventing us from using toString() as designed. If statically-typed languages like Java don't consider "hello" + new Thing() a type error, then I don't think Flow should, at least not in cases we can instruct it to.

@vkurchatkin I'm having this same issue in this situation:

return axios.get(`${process.env.HOST}/questions`);

and unlike most of what's been discussed above, this is getting what I believe should be a string from the environment variable. Adding .toString() didn't fix it for me either.

Here's the precise error I get

Cannot call process.env.HOST because property toString is missing in null or undefined

and my HOST env var is not missing, it works when I disable flow

In this case Flow can't guarantee that HOST will be defined in the environment at runtime, so the type is actually (I assume) ?string.

I'm surprised why Flow even cares since I'm not returning the string. I'm returning the promise and I have this:

myFunc(): Promise{
  return axios.get(`${process.env.HOST}/questions`);
}

You should be getting a separate error for using Promise without a type argument like Promise<string>. But your original error is not about the type of the function's return value anyway, it's about potentially interpolating undefined or null into the string template.

Is there something else I can do instead? Also about that Promise type argument, I should be able to pass Promise<object> then

You have to do something to prove process.env.HOST is an appropriate type for string interpolation.
One example would be:

/* @flow */

const HOST: ?string = Math.random() > 0.5 ? 'foo' : undefined;

type SomeResult = {
  path: string
}

function foo(): Promise<SomeResult> {
  if (typeof HOST !== 'string') {
    throw new TypeError('HOST is undefined'); // try commenting this line out
  }
  return Promise.resolve({
    path: `${HOST}/questions`
  });
}

(flow.org/try of the above)

I really don't get that. But I think the error suggests I do something like this which I don't get either:

env : { [key: string] : ?string };

Current code:

// @flow
import axios from 'axios';

const Request: Object = {
  getFeed() {
    return axios.get(`${process.env.HOST}/questions`);
  },
  authenticate(email: string, password: string) {
    return axios.post(`${process.env.HOST}/auth/sign_in`, { email, password });
  }
};

export default Request;

I get no errors with this:

// @flow
import axios from 'axios';

const Request: Object = {
  getFeed() {
    if (typeof process.env.HOST !== 'string') {
        throw new TypeError();
    }
    return axios.get(`${process.env.HOST}/questions`);
  },
  authenticate(email: string, password: string) {
    if (typeof process.env.HOST !== 'string') {
        throw new TypeError();
    }
    return axios.post(`${process.env.HOST}/auth/sign_in`, { email, password });
  }
};

export default Request;

edit: When testing using a local file. flow.org/try doesn't think it's running in a nodejs environment so it doesn't have the process object declared. Not sure if there's any way to hint that there.

Thanks @vith . That worked

I have an instance of wanting to coerce classes to strings in styled-components templates, for example:

class A {
  copy(): A { return new A() }
  toString(): string { return "Foo" }
}

var instance = new A();

`${instance}`

`${instance.copy()}`

Is this just a bad idea in a type-safe world? Or should there be a way to allow Flow to coerce this class?

For the sake of completeness, TypeScript supports this.

@jjshammas A quick fix I always use is: ${String(instance)} – you can see it working here. It may seem like unnecessary boilerplate but actually I've found I quite like the explicitness, it reminds me that instance is not a string and I'm doing some typecasting there.

@josephrexme your code with process.env.HOST is really a separate issue, this is about allowing guaranteed-to-be-defined objects to be coerced to string inside a template literal.

Is there a workaround here other than "rewrite your JavaScript to comply with arbitrary rules that Flow has created as best practices"?

history.push(`/team/${String(userProfile.userId)}`);
// flow likes this. Nobody writes this kind of code its pretty bad and makes it very unreadable. The whole point of template literals is to ease the pain and help with readable code.

history.push(`/team/${userProfile.userId}`); should be kosher with Flow

Okay, this issue is closed, but why? Does any of linked PR fixes this?
Completely agree with @oshalygin and other participants in this tread.
In case of using custom getters like lodash's get method, this behaviour makes sense and brings inconvenient, in code style and readability in general.
const item = _.get(object, `items.${itemId}`, null); is pretty understandable, for example and cannot throw exception, especially of checking item later.

I don't think this issue should be closed. I am still experiencing it when trying to use a value from Object.entries in a template literal, which is a very typical scenario. Flow makes my code so needlessly convoluted sometimes.

It seems to not work with generics as well

type Node<T> = {
  data: T;
}

function print<T>(node: Node<T>): string {
  return `text_${node.data}_text`;
  //             ^ Cannot coerce `node.data` to string because `T` [1] should not be coerced. [incompatible-type]
}

The following versions works but they introduce worse readability, so no one writes code like that

function print2<T>(node: Node<T>): string {
  return `text_${String(node.data)}_text`;
}

function print3<T>(node: Node<T>): string {
  return `text_${(node.data: any)}_text`;
}

function print4<T>(node: Node<T>): string {
  return `text_${Object.prototype.toString.call(node.data)}_text`;
}

Was this page helpful?
0 / 5 - 0 ratings

Related issues

marcelbeumer picture marcelbeumer  Â·  3Comments

ghost picture ghost  Â·  3Comments

bennoleslie picture bennoleslie  Â·  3Comments

john-gold picture john-gold  Â·  3Comments

jamiebuilds picture jamiebuilds  Â·  3Comments