Checking one conditional React prop does not satisfy TS - javascript

I'm having trouble understanding Typescript. I want to define a <Component/> with one required prop requiredProp, and a condition prop extend which if true, allows to use extendedProp.
e.g.
<Component requiredProp={''} /> // OK
<Component requiredProp={''} extend={true} requiredProp={Function} /> // OK
<Component requiredProp={''} requiredProp={Function} /> // ERROR
<Component requiredProp={''} extend={true} /> // ERROR
My code:
// types
interface RequiredProps {
requiredProp: 'string';
}
type ExtendedProps =
| {
extend?: false;
extendedProp?: never;
}
| {
extend: true;
extendedProp: Function;
};
type ComponentProps = RequiredProps & ExtendedProps;
// component
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
if (extend) {
extendedProp(); // ERROR: Cannot invoke an object which is possibly 'undefined'.
}
}
If I am checking extend to be true, shouldn't TS automatically know from ExtendedProps that extendedProp is also defined?
Compiler is only satisfied if I am explicitly checking extendedProp to be defined:
if (extendedProp) {
extendedProp(); // no error
}

If you do this using the entire props object, it will work. checking for props.extend will narrow down the type on props, and thus props.extendedProp will be allowed:
const Component: React.FC<ComponentProps> = (props) => {
if (props.extend) {
props.extendedProp();
}
}
But if you've already split them up into separate variables, then this can't happen. props isn't involved in your if statement at all, so the type on props can't be narrowed. There are just two unrelated local variables, and checking the type on one doesn't do anything to the type on the other.
You and i can recognize that the two variables are related to eachother, but typescript doesn't have the ability to back track to find the origin of the type, and deduce what that means for other types. It may seem simple in this case, but keep in mind that there are a huge variety of lines of code that could be written that would break the linkage, such as:
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
extend = Math.random() > 0.5
if (extend) {
extendedProp();
}
}
Figuring out the implications of those kinds of things is impractical (or maybe impossible);
Playground link

Related

Defining a generic based on react prop

I'm trying to define a generic type based on the type of a prop.
The implementation looks something like
interface ChildrenProps<T> {
values: T;
errors: T;
}
interface FormHandlerProps<T> {
initialValues: T;
children: React.FC<ChildrenProps<T>>;
}
export function FormHandler<T>({
children,
initialValues,
}: FormHandlerProps<typeof initialValues>) {
const [values] = useState<typeof initialValues>(initialValues)
return(
<>
children({values})
</>
)
}
However when I implement as below, 'values' is defined as 'any'
<FormHandler
initialValues={{name: 'test', address: 'test'}}
>
{({values}) => (
<p> {values.name} </p>
<p> {values.address} </p>
<p> {values.foobar} </p> // SHOULD BE INVALID
)}
</FormHandler>
You can't define the type of initialValues in terms of typeof initialValues; the compiler sees that as circular and doesn't know what to do. It bails out by giving it the any type:
export function FormHandler<T>({
children, initialValues
// -----> ~~~~~~~~~~~~~
// 'initialValues' implicitly has type 'any' because it does not have a type
// annotation and is referenced directly or indirectly in its own initializer.
}: FormHandlerProps<typeof initialValues>) { /* ... */ }
You should have seen a compiler warning to this effect as shown above, assuming you are using the --noImplicitAny or --strict compiler options. If you're not, you really should consider it, since it does a good job of catching errors.
Anyway, the fix here is to just give the argument an explicit type that is not self-referential:
export function FormHandler<T>({
children, initialValues
}: FormHandlerProps<T>) {
const [values] = useState<typeof initialValues>(initialValues)
return (
<>
children({values})
</>
)
}
And then the rest of your code should hopefully work as expected.
Playground link to code

Flow: cant create react component that takes a generic or an object

I am struggling to understand why flow doesn't want me to pase a concrete object type to a react component that may take a generic or ay object of one specific shape.
It complaints that the existing properties on the concrete object are missing on the expected object. But the type I'm passing it is meant to be inferred as the generic one.
I think the problem is that I'm passing an array of one type, while it expects an array of two possible types (even if I don't want to use the second type)
Here is a small reproduction:
import React, { useState, useEffect } from 'react'
const data: {mola: boolean}[] = [{mola: true}]
type Props<T:{}> = {
a: Array<T|{cool: boolean}>
}
const Test = <T:{}>({a}:Props<T>) =>
<div>
{a.map(
(props) => props.cool ?'cool' : 'other'
)}
</div>
const w = () => { return <Test a={data||[{mola: false}]} /> }
This is the error flow gives me:
11: const w = () => { return <Test a={data||[{mola: false}]} /> }
^ Cannot create `Test` element because property `mola`
is missing in object type [1] but exists in object type [2] in array element of property `a`. [prop-missing]
References:
6: a: Array<T|{cool: boolean}>
^ [1]
3: const data: {mola: boolean}[] = [{mola: true}]
^ [2]
And a Try flow repl
EDIT: I added a bit more complex example to make more obvious how I want to use the type
The problem Flow is trying to prevent me from is very well explained on this stackoverflow question
Declaring that the lower component can take an array of a sum type enables that component to push values that fulfill one or the other, like this:
const Test = <T:{}>({a}:Props<T>) => {
a.push({ cool: true })
return (
<div>
{a.map(
(props) => props.cool ?'cool' : 'other'
)}
</div>)
}
Inside the component it is not violating the type, because it is a sum type and it matches one of the types. However, the array that I'm passing down is of one type exclusively. Because of this, flow will not allow you tu pass down such array because there is the risk that it gets "perverted" inside the component.
However, if you declare the array as readOnly, you are guaranteed that nobody will push anything to your precious array, and then you can pass it down.
type Props<T:{}> = {
a: $ReadOnlyArray<T|{ +cool: boolean }>,
}
Fixed example
I think you meant:
type Props<T:{}> = {
a: Array<{cool: boolean}> | Array<T>
}
Try flow

how to process constants in typescript?

I have a react component like so:
const myComponent = ({constant}: Iprops) => (
<div>
{CONSTANTS[constant].property ? <showThis /> : null
</div>
)
it's complaining that element implicitly has an 'any' type because type 'object' has no index signature
how do I add CONSTANTS to my interface? I've tried
interface IProps {
[CONSTANTS: any]: {
constant: boolean;
}
}
but obviously it doesn't like that. how can I declare the type of each key in my object?
thanks
I'm not sure I understanf the shape of the objects you actually need. But you can type an object with this syntax:
const CONSTANTS: { [key:string]: boolean } = {}
CONSTANTS["something1"] = false; // ok
CONSTANTS["something2"] = "Hey"; // not ok
It's a bit tricky as the key can actually be either string or number but the value type is properly enforced so you can still have more complex types there instead of a boolean.

Flow: is not a polymorphic type

i just integrated flow for the first time to check my javascript sources statically.
I am struggling with a error flow finds and i am not able to solve it on my own. Its about using es6 classes and inheritance. More specific i created some react Components and they should inherit some methods.
I have a Callout Component, that represents a callout message of unspecified severity. To make things a little more simple i thought about providing a ErrorMessage Component, that inherits the Callout Component. My classes Structure looks like:
React.Component
> AbstractComponent (here i add some project-wide helpers for i18n and so on
> Callout (this represents a pretty message on the screen)
> ErrorMessage (this represents an error)
Flow tells me:
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/js/Components/Generic/ErrorMessage.js:14:43
statics of Callout [1] is not a polymorphic type.
11│ icon: string
12│ };
13│
[1] 14│ export default class ErrorMessage extends Callout<Props> {
15│
16│ static get defaultProps(): Props {
17│ return {
The part Callout<Props> gets highlighted
I already define the Props Type for the Callout class, so this might be the problem but i cant solve it on my own.
A similar error is thrown a few lines below, where i try to access a parent method by addressing super.content (content is a get-method of Callout).
Thanks in advance
UPDATE: Why do i want to use class inheritance?
The inheritance Callout > ErrorMessage just exists to reduce redundant code, but its not necessary, so lets ignore this and talk about a more common case:
I want to have a class AbstractComponent to make common things in my project easier.
Some examples:
Printing of translation strings: In order to make the component multilingual, i created a utility class to generate translation strings, inside a component it works like
function render() {
return (
<div>
{new Translation(
'namespace',
'key',
[some, args],
`${some} fallback message with optional ${args}`
).toString()}
</div>
)
}
In order to use this, every component in my stack ends up with the import statement on top
import Translation from "../Core/Translation"
or in the best case
import t from "../Core/Translation"
I use webpack to build a bundle and webpack seems to blow up the compiled javascript with every import statement you use. So i figured - to reduce coding effort and bundle size - i provide a intermediate component class, that adds some utility methods like:
class AbstractComponent extends React.Component {
constructor(props) {
super(props);
this.logger = props.logger || new Logger();
this.api: ApiInterface = props.api || new MockApi();
}
translate(namespace: string, key: string, args: ?[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
svgSprite(id: string, className: string = "") {
return (
<SvgSprite id={id} className={className} />
)
}
}
I also added some other things to show you more reason for a intermediate Component class.
So, all of this works! But flow complains about missing return types and so on, thats good with me, for that purpose i want to use flow! The problem i cant solve is the inheritance itself... But for me it does make a lot of sense.
If you really want to deal with inheritance (which I don't have an issue with, I just feel like you will probably run into issues later), you can do something like the following:
class AbstractComponent<Props: {}, State: ?{} = null> extends React.Component<Props, State> {
api: ApiInterface
logger: typeof Logger
constructor(props) {
super(props);
this.logger = props.logger || new Logger();
this.api = props.api || new MockApi();
}
translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
svgSprite(id: string, className: string = "") {
return (
<SvgSprite id={id} className={className} />
)
}
}
And use it like:
class Test extends AbstractComponent<{ some: string, args: string }> {
render() {
const { some, args } = this.props
return (
<div>
{this.translate(
'namespace',
'key',
[some, args],
`${some} fallback message with optional ${args}`
)}
</div>
)
}
}
Now, I will say that to some extent I understand where Facebook is coming from. Your component in this case is really already an abstract construct. And if you want this to be more flexible (let's say you have a stateless component that could benefit from having a logger and a translate function), you could do one of two things:
This is the defined type and translate function I'm using in both:
type CommonProps = {
logger?: Logger,
api?: ApiInterface,
translate?: (namespace: string, key: string, args: ?string[], fallback: ?string) => string
}
// This should look familiar
function translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
Higher order component
function addCommonStuff({ logger = new Logger(), api = new MockApi(), translate = translate }: CommonProps) {
return <Props: {}>(
WrappedComponent: ComponentType<Props>
): ComponentType<
$Diff<Props, $NonMaybeType<CommonProps>>
> => (props: Props) => <WrappedComponent {...props} logger={logger} api={api} translate={translate} />
}
And used like:
class Test extends React.Component<{}> {}
const TestWithCommons = addCommonStuff({})(Test)
;<TestWithCommons />
Reusable component with a render prop
class Common extends React.Component<CommonProps & { render?: Function, children?: Function }, $NonMaybeType<CommonProps>> {
state = {
logger: this.props.logger || new Logger(),
api: this.props.api || new MockApi(),
translate: translate
}
render() {
const { children, render } = this.props
return typeof render === 'function' ? render(this.state) : (
typeof children === 'function' ? children(this.state) : null
)
}
}
And use it like this:
class TestCommon extends React.Component<{}> {
render() {
return <Common>
{({ logger, api, translate }) => translate('namespace',
'key',
null,
`Fallback message`
)}
</Common>
}
}
As an aside to this discussion, you don't need to write defaultProps as a getter for your callout. static defaultProps = {} should be enough. It shouldn't take passed in props into account or anything. If it does, you're better off using state

Storing objects with prototype functions in Redux state

Is it considered bad practice to store classes in the state? I've read that the state should be easily serializable objects and classes break this convention.
I'm creating a web application that uses React/Redux with a set of backing models to collect information from the user.
So for example I have the following model:
class Person {
constructor(params = {}) {
this.firstName = params.firstName || '';
this.lastName = params.lastName || '';
this.gender = params.gender || '';
this.dateOfBirth = params.dateOfBirth || '';
this.stateOfResidence = params.stateOfResidence || '';
this.email = params.email || '';
}
validateField(args) {
// remove for brevity
}
valid(args) {
// remove for brevity
}
}
The object is added to the store by issuing the following actionCreator:
export function addPerson(args = {}) {
return (dispatch) => {
dispatch({
type: 'ADD_PERSON',
obj: new Person(args)
});
};
}
The appropriate person reducer sticks it into the state so I can get at it from the rest of the application.
This way when I grab the object from the state I can run it's validate and valid prototype functions to see if the model is valid and react appropriately.
Does this break React/Redux convention? What would be another way to approach this problem? What are the potential problems I might run into down the road with the above approach? Right now can't foresee any problems... but I also don't have much experience with React/Redux (~4 months)
Edit:
I should add that I only mutate the object state through the reducers. I am careful to not mutate state elsewhere in the application.
Yes, it's generally considered an anti-pattern, because it breaks the ability to do things like time-travel debugging (one of Redux's core selling points). See the Redux FAQ at for further details.
If you do want to have more of an object-like facade over your plain data, you may want to look into something like redux-orm.
so i think my code help someone need to find a way can be solve transform class to function by difficult function doesn't support protypes let some my code can fix them and easy with
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px',
}}
>
{children}
</button>
)
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link

Categories

Resources