Flow: Type check doesn't work for constants

Created on 1 Sep 2016  Â·  26Comments  Â·  Source: facebook/flow

I have an issue with a flow check on a prop type:

// ButtonInputs.js

export const ButtonTypes = {
  PRIMARY: 'primary',
  PRIMARY_SPECIAL: 'primary-special',
}

export type Button =
  | ButtonTypes.PRIMARY
  | ButtonTypes.PRIMARY_SPECIAL;

// MyCustomButton.js

import type { Button } from 'ButtonInputs';

type Props = {
  type: Button;
}

<MyCustomButton type="test" /> // WORKS! (shouldn't work)

However, when changing the constants back to normal strings, the flow check works as expected.

export type Button =
  | 'primary'
  | 'primary-special';

Any idea why this is the case? Is this a known bug?

Most helpful comment

@nmn That's not really helpful, is it?

Quoting the flowtype docs:

Because Flow understands JavaScript so well, it doesn’t need many of these types. You should only ever have to do a minimal amount of work to describe your code to Flow and it will infer the rest. A lot of the time, Flow can understand your code without any types at all.

minimal amount of work.

I use flowtype because it's helping me write readable code.

This:

export const ButtonTypes = {
  PRIMARY: 'PRIMARY',
  PRIMARY_SPECIAL: 'PRIMARY_SPECIAL',
}

export type Button = $Keys<typeof ButtonTypes>

Is inferior in every way to this:

const PRIMARY = 1;
const PRIMARY_SPECIAL = 2;
export type Button = PRIMARY | PRIMARY_SPECIAL

or something like this:

const PRIMARY = 1;
const PRIMARY_SPECIAL = 2;
const TYPES = [PRIMARY, PRIMARY_SPECIAL]
export type Button = <unionof TYPES>;

All 26 comments

You can't do this

export type Button =
  | ButtonTypes.PRIMARY
  | ButtonTypes.PRIMARY_SPECIAL;

Flow raises

src/index.js:11
 11:   | ButtonTypes.PRIMARY
         ^^^^^^^^^^^^^^^^^^^ string. Ineligible value used in/as type annotation (did you forget 'typeof'?)
 11:   | ButtonTypes.PRIMARY
         ^^^^^^^^^^^^^^^^^^^ PRIMARY

src/index.js:12
 12:   | ButtonTypes.PRIMARY_SPECIAL;
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ string. Ineligible value used in/as type annotation (did you forget 'typeof'?)
 12:   | ButtonTypes.PRIMARY_SPECIAL;
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PRIMARY_SPECIAL

Even adding typeof

export type Button =
  | typeof ButtonTypes.PRIMARY
  | typeof ButtonTypes.PRIMARY_SPECIAL;

is not what you want, since typeof ButtonTypes.PRIMARY is string, thus <MyCustomButton type="test" /> still type checks

I have the same question. Is this by design and I shouldn't have Enums anymore as long as my magic strings are type checked? Or there's a special way to type check enum values w/ flow?

I guess it supposed to work like this: in the type definitions only actual strings must be used and in the checked code we can use variables / Enums.

The same problem with

const KIND_OBJECT = 1;
const KIND_FIELD = 2;

type FieldKindsT = KIND_OBJECT | KIND_FIELD;

returns error

Ineligible value used in/as type annotation (did you forget 'typeof'?)
KIND_OBJECT: app/_components/Form/schema/formSchema.js:8

But such record works perfectly

type FormSchemaFieldTypesT = 'String' | 'Float' | 'Int' | 'Boolean' | 'Date';
const a: FormSchemaFieldTypesT = 'String'; // no error
const b: FormSchemaFieldTypesT = 'String123'; // expected error "This type is incompatible with string enum"

Also converting kinds to string does not work:

const KIND_OBJECT = '1';
const KIND_FIELD = '2';

type FieldKindsT = KIND_OBJECT | KIND_FIELD;

Finally using now such not perfect, but working solution:

const KIND_OBJECT = 1;
const KIND_FIELD = 2;

type FieldKindsT = 1 | 2;

const a: FieldKindsT = 1; // no error
const b: FieldKindsT = 15; // expected error "This type is incompatible with number enum"

Slightly better:

const KIND_OBJECT: 1 = 1;
const KIND_FIELD: 2 = 2;

type FieldKindsT = typeof KIND_OBJECT | typeof KIND_FIELD;

@vkurchatkin thanks!
A little bit crazy view, but quite applicable.

I ran into a similar issue trying to use imported constants to compute object/type property names.

const Constants = {
    A: "asdf",
    B: "other"
}

type TwoLiteral = {
  asdf: Array<string>,
  other: string
}

type TwoCompLiteral = {
  [Constants.A]: Array<string>,
  [Constants.B]: string
}

const testLiteral: TwoLiteral = {
  [Constants.A]: ["hello", "world"],
  other: "hello"
};

const testComputed: TwoCompLiteral = {
  asdf: ["hello", "world"],
  [Constants.B]: "hello"
};
12:   [Constants.A]: Array<string>,
       ^ string. Ineligible value used in/as type annotation (did you forget 'typeof'?)
12:   [Constants.A]: Array<string>,
       ^ A
13:   [Constants.B]: string      ^ multiple indexers are not supported
23:   [Constants.B]: "hello"                     ^ string. This type is incompatible with
12:   [Constants.A]: Array<string>,
                     ^ array type

Try Flow

It's the same problem if I explicitly set the types of the constants to literals. Flow seems to be using the computed property syntax to allow for Map index generation.

any update on the fix?

There is no real bug here. Look at @vkurchatkin's comment for the current best way to do this.

You can also use $Keys to make this easier when you always have the same values for the keys and values in your object.

export const ButtonTypes = {
  PRIMARY: 'PRIMARY',
  PRIMARY_SPECIAL: 'PRIMARY_SPECIAL',
}

export type Button = $Keys<typeof ButtonTypes>

I'm trying out Flow and am trying to use it with Redux. I'd like to setup my action creators in a way that flow can verify I've supplied the right type attribute? For example, if I've setup my flow type as export type SetNotificationAction = TypedAction<'notification:setNotification', string> I want to only be allowed to return {type: 'notification:setNotification', ...}

export function setNotification(message: string): SetNotificationAction {
  return {
    type: 'would be nice if any string didn't pass here',
    payload: message
  };
}

I obviously can't do const SET_NOTIFICATION = 'notification:setNotification' and export type SetNotificationAction = TypedAction<SET_NOTIFICATION, string>. I can't use $Keys because that would mess up my reducer.

For example, it'd be great if Flow could catch this bug (in the reducer CLEAR_NOTIFICATION returns a payload that's undefined and message should always be a string. CLEAR_NOTIFICATION should be SET_NOTIFICATION HERE) :

/* @flow */
export const actionTypes = {
  SET_NOTIFICATION: 'notification:setNotification',
  CLEAR_NOTIFICATION: 'notification:clearNotification'
};

type TypedAction<T, P> = {
  type: T,
  payload: P
};

export type SetNotificationAction = TypedAction<'notification:setNotification', string>;
export type ClearNotificationAction = TypedAction<'notification:clearNotification', void>;

export type Action =
  | SetNotificationAction
  | ClearNotificationAction;

export function clearNotification(): ClearNotificationAction {
  return {
    type: actionTypes.CLEAR_NOTIFICATION,
    payload: undefined
  };
}

export function setNotification(message: string): SetNotificationAction {
  return {
    type: actionTypes.SET_NOTIFICATION,
    payload: message
  };
}

export type State = { message: string };

const initialState: State = { message: ''};

export default function reducer(state: State = initialState, action: Action): State {
  switch (action.type) {
    case actionTypes.CLEAR_NOTIFICATION:
      return { message: action.payload };

    case actionTypes.CLEAR_NOTIFICATION:
      return initialState;

    default:
      return state;
  }
}

@nmn That's not really helpful, is it?

Quoting the flowtype docs:

Because Flow understands JavaScript so well, it doesn’t need many of these types. You should only ever have to do a minimal amount of work to describe your code to Flow and it will infer the rest. A lot of the time, Flow can understand your code without any types at all.

minimal amount of work.

I use flowtype because it's helping me write readable code.

This:

export const ButtonTypes = {
  PRIMARY: 'PRIMARY',
  PRIMARY_SPECIAL: 'PRIMARY_SPECIAL',
}

export type Button = $Keys<typeof ButtonTypes>

Is inferior in every way to this:

const PRIMARY = 1;
const PRIMARY_SPECIAL = 2;
export type Button = PRIMARY | PRIMARY_SPECIAL

or something like this:

const PRIMARY = 1;
const PRIMARY_SPECIAL = 2;
const TYPES = [PRIMARY, PRIMARY_SPECIAL]
export type Button = <unionof TYPES>;

How are people addressing this issue in their code?

What I am currently using (workaround?) is something like this:

// @flow
export const PRIMARY: string & 'primary' = 'primary';
export const SECONDARY: string & 'secondary' = 'secondary';
export type Button = typeof PRIMARY | typeof SECONDARY;

@geraldyeo Thanks, useful trick! It seems to me it can be simplified further:

// @flow
export const PRIMARY: 'primary' = 'primary';
export const SECONDARY: 'secondary' = 'secondary';
export type Button = typeof PRIMARY | typeof SECONDARY;

If you ask me, the official syntax makes more sense:

export const ButtonTypes = {
  PRIMARY: 'PRIMARY',
  SECONDARY: 'SECONDARY',
  PRIMARY_SPECIAL: 'PRIMARY_SPECIAL',
};

type Props = {
  type?: $Keys<typeof ButtonTypes>,
};

export default ({ type = ButtonTypes.SECONDARY, ...otherProps }: Props) => (
  <button type={type} {...otherProps} />
);

This way in an external file you can do the following:

import Button, { ButtonTypes } from 'components/Button';

export default() => (
  <Button type={ButtonTypes.PRIMARY)>
    Button label
  </Button>
);

@nmn there are really common use cases that would benefit from allowing this kind of syntax in a compact way. typing actions and reducers inside redux applications would become a lot nicer and safer for instance

This is what we've come up with:

const ANIMAL = {
  DOG: ('dog': 'dog'),
  CAT: ('cat': 'cat'),
};

type Animal = $Values<typeof ANIMAL>;

const success: Animal = ANIMAL.DOG;
const failure: Animal = 'hog';

Thanks to @fagerbua , I improved the code just like this

// @flow

const ButtonTypes:
{
    PRIMARY: 0,
    SECONDARY: 1,
    PRIMARY_SPECIAL: 2,
} = {
    PRIMARY: 0,
    SECONDARY: 1,
    PRIMARY_SPECIAL: 2,
}

type Props = $Values<typeof ButtonTypes>;

function show(buttonType: Props) {
    console.log(buttonType);
}

show(ButtonTypes.PRIMARY); // No Error
show(3); // This type is incompatible with the expected param type of number enum

@Naoto-Ida, @ckitterl: Using $Keys gives much simpler error messages, see flow.org/try

@guidobouman You may misunderstand the $keys usage。 The $Keys just retrun T type's key set. See the example below

// @flow
const countries = {
  US: "United States",
  IT: "Italy",
  FR: "France"
};

type Country = $Keys<typeof countries>;

const italy: Country = 'IT';
const nope: Country = 'nope'; // 'nope' is not a Country

Country type is the union of US,ITANDFR 。My last example show how to get a Country type that combine United States, Italy AND France

The ButtonTypes of Your example by coincidence have same key and value。 see try it

@ckitterl I'm well aware of that fact. To me duplication of the whole list of types really feels like a no go. I'd much rather use keys with the same values. But this is just personal preference.

What would actually improve this situation would be an $ExactValues helper. This would allow using the exact values instead of value types.

Proposed:

// @flow
const countries = {
  US: "United States",
  IT: "Italy",
  FR: "France"
};

type Country = $ExactValues<typeof countries>;

const italy1: Country = countries.IT; // This would work
const italy2: Country = 'Italy'; // Ideally this would fail, but that's out of scope
const nope: Country = 'nope'; // 'nope' is not a Country

@guidobouman It works actually. You just need to freeze the object (or pretend you freeze it)

https://flow.org/try/#0PTAEAEDMBsHsHcBQiDGsB2BnALqNBXdbAJwEsBTTUAXlGACoAuRgeQCMArclbAOkmLlyAL3IAKesADeiUKACqAZUagARPPSls5ACahF2AIbbMqgDSzQASQAqK1VaPQAnucsAxAEr33xQ+hRyVUQAXwZmAEpJAG5kbGcAB3JQAGFYQhJnGlAAEgA1Q2h8SgAeeKTYSDx0ojJKAD5Y1AwcUC1C5wBGFTSM4izaAlqKTF5baLowGwALUip4dOg9BeIAa2asXHaXACYemszsgHJHDqOJkGsdckKXUGxZ+cW9SENSaDNQNnxcB+MjqjpXCVUCYNBJDatdCwJL7PoDUBHaFJc6TRHI8hHNpUaG4QypA79IA

Oh, that's more like it! 👏

PS: On a lower level this might get fixed in a different way: https://github.com/facebook/flow/issues/2639

@TrySound It's work under version of 0.6x.y but not 0.5x.y. I run my script by 0.59.0, but, thank you all the samle ,I will td my flow

Was this page helpful?
0 / 5 - 0 ratings