I have a component with both images and text.
<script lang="ts">
/** Import pacakges */
/** Props */
export let align: 'left' | 'center' = 'left';
export let bgColor: 'bg-primary' | 'bg-secondary' | 'bg-offwhite' = 'bg-offwhite'
export let hasImage: boolean = false;
export let imageAlign: 'left' | 'right' = 'left';
</script>
I want to export a prop imgSrc only if the hasImage property is true. Is it possible? Are there any other methods to achieve this?
You have to always export the property, as exports have to appear at the top level.
The only thing you can try to do is adjust the types. There is a way to type all properties via defining a $$Props type but I have not been able to make that work, as it does not resolve unions correctly.
You can introduce generics however and infer the type of other properties based on the resolved generic type. This comment on the feedback issue pointed me towards this approach:
<script lang="ts">
type HasImage = $$Generic<boolean | undefined>;
// other props...
export let hasImage: HasImage = false as HasImage;
export let imgSrc: Exclude<HasImage, undefined> extends true ? string : never;
</script>
Usage examples:
<Component hasImage imgSrc="x" />
<Component hasImage={false} imgSrc="x" /> <!-- Error -->
<Component hasImage={true} imgSrc="x" />
<Component imgSrc="x" /> <!-- Error -->
The errors are:
Type 'string' is not assignable to type 'never'. ts(2322)
RFC for the generics feature
Related
The Setup
I have a heading component whose types look like this.
// Heading/index.d.ts
import { HTMLAttributes } from 'react';
export const HeadingType: {
product: 'product';
marketing: 'marketing';
};
export const HeadingLevel: {
h1: 'h1';
h2: 'h2';
h3: 'h3';
h4: 'h4';
h5: 'h5';
h6: 'h6';
};
export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
as: keyof typeof HeadingLevel;
type?: keyof typeof HeadingType;
}
export const Heading: React.FC<HeadingProps>;
Here's what the as type gets inferred as, which is what I want because that makes the intellisense work really well at the consumer side.
Inference
Component Usage
The problem is that this doesn't work if I have to generate the element in a loop using let's say, map because the actual data I wanna use may be string.
The error I am given on hovering over as is
Type '`h${number}`' is not assignable to type '"h1" | "h2" | "h3" | "h4" | "h5" | "h6"'.ts(2322)
index.d.ts(17, 3): The expected type comes from property 'as' which is declared here on type 'IntrinsicAttributes & HeadingProps & { children?: ReactNode; }'
The Question
How do I type this so both the cases work?
Notes
If you are wondering why I have an export const HeadingLevel in the index.d.ts file and simply the union type is because the actual component is written in JS, which exports two variables with the same name that look like this.
You need to build some kind of Loose Completion type helper like this
export const HeadingType: {
product: 'product';
marketing: 'marketing';
};
export const HeadingLevel: {
h1: 'h1';
h2: 'h2';
h3: 'h3';
h4: 'h4';
h5: 'h5';
h6: 'h6';
};
type HeadingLevelUnion = keyof typeof HeadingLevel;
type HeadingTypeUnion = keyof typeof HeadingType;
export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
as: HeadingLevelUnion | Omit<string, HeadingLevelUnion>;
type?: HeadingTypeUnion | Omit<string, HeadingTypeUnion>;
}
Reference : MattPocock Loose Autocomplete
I am trying to give a conditional colour to a component in react if the type is like it will be green or dislike it will be red. This would work in javascript but with using typescript I am getting the error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ like: string; dislike: string; }'. No index signature with a parameter of type 'string' was found on type '{ like: string; dislike: string; }'.
I am fairly new to typescript in react so I am not too sure how to solve this so any help would be great!
import React from "react";
import {Text, View} from "react-native";
import {styles} from "./styles";
const COLORS = {
like: '#00edad',
dislike: '#ff006f',
}
const Choice = ({type} : {type : string}) => {
const color = COLORS[type];
return (
<View style={[styles.container, {borderColor: color}]}>
<Text style={[styles.text, {color}]}>{type}</Text>
</View>
)
}
export default Choice;
Your COLORS object defines 2 key value pairs: "like" and "dislike". If you try to access COLORS["foobar"], it will give you a typescript error as it is not a defined key.
Similarly, your type is defined as a string and not constrained to only "like" or "dislike". To constrain it, you need to only allow the "like" or "dislike" keys for it.
One way to do it is to say:
const Choice = ({type} : {type : "like" | "dislike" }) => {
A more robust way is to make the possible values based on your object.
const Choice = ({type} : {type : keyof typeof COLORS}) => {
As a side note, you React gives you generic types you can use it make you type expressions less cluttered:
const Choice: React.FC<{type : keyof typeof COLORS}> = ({type}) => {
I am building a library of components and I need some of them to have a customizable tag name. For example, sometimes what looks like a <button> is actually a <a>. So I would like to be able to use the button component like so:
<Button onClick={onClick}>Click me!</Button>
<Button as="a" href="/some-url">Click me!</Button>
Ideally, I would like the available props to be inferred based on the "as" prop:
// Throws an error because the default value of "as" is "button",
// which doesn't accept the "href" attribute.
<Button href="/some-url">Click me!<Button>
We might need to pass a custom component as well:
// Doesn't throw an error because RouterLink has a "to" prop
<Button as={RouterLink} to="/">Click me!</Button>
Here's the implementation, without TypeScript:
function Button({ as = "button", children, ...props }) {
return React.createElement(as, props, children);
}
So, how can I implement a "as" prop with TypeScript while passing down the props?
Note: I am basically trying to do what styled-components does. But we are using CSS modules and SCSS so I can't afford adding styled-components. I am open to simpler alternatives, though.
New answer
I recently came across Iskander Samatov's article React polymorphic components with TypeScript in which they share a more complete and simpler solution:
import * as React from "react";
interface ButtonProps<T extends React.ElementType> {
as?: T;
children?: React.ReactNode;
}
function Button<T extends React.ElementType = "button">({
as,
...props
}:
ButtonProps<T>
& Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>
) {
const Component = as || "button";
return <Component {...props} />;
}
Typescript playground
Old answer
I spent some time digging into styled-components' types declarations. I was able to extract the minimum required code, here it is:
import * as React from "react";
import { Link } from "react-router-dom";
type CustomComponentProps<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> = React.ComponentPropsWithRef<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any> ? C : never
> &
O & { as?: C };
interface CustomComponent<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> {
<AsC extends keyof JSX.IntrinsicElements | React.ComponentType<any> = C>(
props: CustomComponentProps<AsC, O>
): React.ReactElement<CustomComponentProps<AsC, O>>;
}
const Button: CustomComponent<"button", { variant: "primary" }> = (props) => (
<button {...props} />
);
<Button variant="primary">Test</Button>;
<Button variant="primary" to="/test">
Test
</Button>;
<Button variant="primary" as={Link} to="/test">
Test
</Button>;
<Button variant="primary" as={Link}>
Test
</Button>;
TypeScript playground
I removed a lot of stuff from styled-components which is way more complex than that. For example, they have some workaround to deal with class components which I removed. So this snippet might need to be customized for advanced use cases.
I found that you can make the same thing with JSX.IntrinsicElements. I have Panel element:
export type PanelAsKeys = 'div' | 'label'
export type PanelAsKey = Extract<keyof JSX.IntrinsicElements, PanelAsKeys>
export type PanelAs<T extends PanelAsKey> = JSX.IntrinsicElements[T]
export type PanelAsProps<T extends PanelAsKey> = Omit<PanelAs<T>, 'className' | 'ref'>
I omitted native types like ref and className because i have my own types for these fields
And this how props will look look like
export type PanelProps<T extends PanelAsKey = 'div'> = PanelAsProps<T> & {}
const Panel = <T extends PanelAsKey = 'div'>(props: PanelProps<T>) => {}
Then you can use React.createElement
return React.createElement(
as || 'div',
{
tabIndex: tabIndex || -1,
onClick: handleClick,
onFocus: handleFocus,
onBlur: handleBlur,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
'data-testid': 'panel',
...rest
},
renderChildren)
I see no ts errors in this case and have completions for label props like htmlFor
I'm trying to create a generic Button component in react.
To help keep things generic, I want to be able to choose what Element is used to render the button. Whether that be a standard HTML button, an anchor element or a span. This is passed into the component props as a string. ButtonProps is below:
type ButtonProps = {
component?: string;
variant: 'primary' | 'secondary' | 'default' | 'text';
children: JSX.Element | string;
classes?: string;
};
I'm then setting the default value of the component prop to 'button'. And trying to render:
/**
*
* Stateless component for buttons
* #export
* #param {ButtonProps} props
*/
export default function Button(props: ButtonProps): JSX.Element {
const { component = 'button', classes = clsx(), children } = props;
const ComponentProp = component;
return <ComponentProp>{children}</ComponentProp>;
}
TypeScript gives the error Type '{ children: string | Element; }' has no properties in common with type 'IntrinsicAttributes'.
How do I appease TS? Or is this just a stupid way to be doing things?
So I have been playing around with type systems in JavaScript and for the most part things are working however there is an issue with styled-components. I can't seem to find a good way to apply flow to the props of a styled-component. So far the only solution I see is:
export type ButtonPropTypes = ReactPropTypes & {
styleType: 'safe' | 'info' | 'warning' | 'danger' | 'link',
isPill: boolean,
isThin: boolean,
};
export const ButtonStyled = styled.button`
${generateBaseStyles}
${hoverStyles}
${fillStyles}
${thinStyles}
${linkStyles}
`;
export const Button = (props: ButtonPropTypes) => <ButtonStyled {...props} />;
It seems pretty excessive that I have to create 2 component for every styled component.
I am hoping my google skills are just crap and I am missing something but is there a better way to do this other than multiple components per styled component?
Yes! There is a better way. The trick is to declare the type of the component created by styled-components. You can do this by casting the result returned by styled.button`...` to the type of a React component that takes in your desired props. You can generate the type of a React component that takes in arbitrary props with type mytype = React.ComponentType<MyProps>.
// #flow
import styled from 'styled-components'
// Make sure you import with * to import the types too
import * as React from 'react'
// Mock function to use styleType
const makeStyles = ({styleType}) => ''
export type ButtonPropTypes = {
styleType: 'safe' | 'info' | 'warning' | 'danger' | 'link',
isPill: boolean,
isThin: boolean,
};
export const ButtonStyled = (styled.button`
${makeStyles}
${({isPill}) => isPill ? 'display: block;' : ''}
${({isThin}) => isThin ? 'height: 10px;' : 'height: 100px;'}
`: React.ComponentType<ButtonPropTypes>) // Here's the cast
const CorrectUsage = <ButtonStyled styleType="safe" isPill isThin/>
const CausesError = <ButtonStyled styleType="oops" isPill isThin/> // error
const CausesError2 = <ButtonStyled styleType="safe" isPill="abc" isThin={123}/> // error
I've hosted the code on GitHub for local reproduction (since Flow's sandbox doesn't work with external dependencies): https://github.com/jameskraus/flow-example-of-styled-components-props
In addition to James Kraus' answer, if you're using flow-typed (and have installed the package for your version of styled-components) you can essentially:
import styled, {type ReactComponentStyled} from 'styled-components'
type Props = {
color?: string
}
const Button: ReactComponentStyled<Props> = styled.button`
color: ${({color}) => color || 'hotpink'};
`