I have a component, It takes arguments such as :
interface Props {
label: string;
children?: React.ReactNode;
withoutActions?: boolean;
fieldKey?: KeyProperties;
corporate: Corporate;
}
The withoutActions and fieldKey are used by a form.
If the withoutActions prop is true, then there should not be a provided fieldKey.
However, if my withoutActions is undefined, then i should enforce my fieldLabel to be of type **KeyProperties** which is a specific list of available values for it.
If withoutActions is undefined, the other SHOULD NOT be defined at all
If withoutActions is true, the other SHOULD be defined respecting the specific keyProperties type.
How can i implement that ?
Similar to the other answer, but you can refactor it a bit to be cleaner. You did not specify what the expected behavior is if withoutActions is false instead of true or undefined. Here, I assume the behavior for false and undefined are the same. If false is not a valid type, you could just swap withoutActions?: false for withoutActions: undefined like the other answer.
type Props = {
label: string;
children?: React.ReactNode;
corporate: Corporate;
} & (
{
withoutActions: true;
} |
{
withoutActions?: false;
fieldKey: KeyProperties;
}
)
However, there is an important pitfall here that you should be aware of. Because of TypeScript's structural typing, you only get excess property checking when you are directly assigning an object literal. You do not get excess property checking when you assign an an object as an inferred type. TypeScript and React treat direct props declarations as if they are object literals, and will do excess property checking like you seem to desire. However in some cases, if you assign objects to variables and let their type be inferred, TypeScript may not warn that there is an excess property present.
Check out this demo based on your original example. Example #1 and #2 will error because of excess property checking, but example #3 will not.
const ExampleOne = () => {
// Type error - excess property checking
return <Component label={''} corporate={''} withoutActions fieldKey={''} />;
}
const ExampleTwo = () => {
const props: Props = {
label: '',
corporate: '',
withoutActions: true,
// Type error - excess property checking
fieldKey: '',
}
return <Component {...props} />;
}
const ExampleThree = () => {
const props = {
label: '',
corporate: '',
withoutActions: true,
fieldKey: '',
}
// No type error - no excess property checking
return <Component {...props} />;
}
I did not fully understand your requirement, but I would use a type alias instead of an interface. E.g. something like this:
type Props = {
label: string,
children?: React.ReactNode,
withoutActions: true,
corporate: Corporate
} | {
label: string,
children?: React.ReactNode,
withoutActions: undefined,
fieldKey: KeyProperties,
corporate: Corporate
}
Related
I created a stencil component () that uses another stencil component () in the rendered template.
One of the props (key: string) passed from smg-compound-filter to smg-filter is undefined, whereas the other props are well defined. I made sure that {filter.key} is defined in the template of smg-compound-filter, and i even tried to pass a literal string to smg-filter, but it's undefined in this component. I think i am missing something. If someone could give me an insight, it will be bery helpful.
smg-compound-filter.ts (render function)
render() {
return (
<div class="smg-filter-container">
<div class={`filters`}>
this.filters.map(filter => {
return (
<div class='filter'>
<smg-filter
key={filter.key} // even "str" doesn't work
label={filter.label}
target={filter.target}
options={filter.options}
customClasses='secondary'
onFilterChanged={(event) => {this.toggleOption(event)}}
>
</smg-filter>
</div>
);
})
</div>
</div>
);
}
smg-filter.ts
export class SmgFilter {
#State() filter: Filter;
#State() showOptions: boolean;
/** custom classes to adapt style */
#Prop() customClasses: string;
/** smartsheet column linked to the filter */
#Prop() key: string;
/** label of the filter */
#Prop() label: string;
/** options */
#Prop() options: FilterOption[];
/** type of products to be filtered: 'master' or 'product' */
#Prop() target: FilterTarget;
#Event() filterChanged: EventEmitter<Filter>;
componentWillLoad() {
console.log(this.key); // undefined
this.showOptions = !isSecondary ? true : false;
this.filter = {
key: this.key,
target: this.target,
label: this.label,
options: this.options,
};
}
That's because key is a reserved keyword since Stencil uses it to improve loop rerender performance.
I'm not 100% sure if it can be changed or disabled but I think you'll have to use another property name.
The #Prop() name "key" is a reserved public name. Please rename the "key" prop so it does not conflict with
an existing standardized prototype member. Reusing prop names that are already defined on the element's
prototype may cause unexpected runtime errors or user-interface issues on various browsers, so it's best to
avoid them entirely.
I have a componenet:
type Base = {
color: string
}
type Button = {
to: string
} & Base
type Link = {
link: string
linkNewTab: boolean
} & Base
type ComponentProps = Button | Link
export const Button: React.FC<ComponentProps> = (props) => {
return (<>
{props.link ? <Link newTab={props.linkNewTab}>...</Link> : props.to && <Button>...</Button>}
</>)
}
There is a base type with props which both types have in common.
The Component should either has Button or Link type based on the given props. But if it has Link, these props are preferred.
Typescript Error:
Property 'link' does not exist on type 'PropsWithChildren<ComponentProps>'.
Property 'link' does not exist on type '{ to: string; } & Base & { children?: ReactNode; }'.ts(2339)
What I don't want:
I can solve the problem by adding a type to the Base type and decide from their which props are allowed. I would like to automatically decide that based on props.
Information: 4.5.4 TypeScript Version
The problem is that you are attempting to access a value at property which might not exist. Instead, you can check to see if that property exists in the object (by using the in operator) before trying to access its value. This will also discriminate the union:
TS Playground
// instead of this:
if (props.link) {}
// ^^^^
// Property 'link' does not exist on type 'Button | Link'.
// Property 'link' does not exist on type 'Button'.(2339)
// prop in obj
if ('link' in props) {
props.color // ok
props.link // ok
props.linkNewTab // ok
}
else {
props.color // ok
props.to // ok
}
You can check whether the link property exists by using the in operator. This will narrow down the type without accessing the property, and so typescript will allow it:
<>
{"link" in props ? (
<Link newTab={props.linkNewTab}>...</Link>
) : (
"to" in props && <Button>...</Button>
)}
</>
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
I've the following types
type ItemDefaultType = object | null | string
interface ItemToString<Item = ItemDefaultType> {
(item: ItemDefaultType): string;
}
interface AutosuggestState<Item = ItemDefaultType> {
highlightedIndex: number | null
inputValue: string | null
isOpen: boolean
selectedItem: Item
}
interface AutosuggestProps<Item = ItemDefaultType> extends AutosuggestState<Item> {
itemToString?: ItemToString<Item>;
initialSelectedItem?: Item;
initialInputValue?: string;
initialHighlightedIndex?: number | null;
initialIsOpen?: boolean;
defaultHighlightedIndex?: number | null;
defaultIsOpen?: boolean;
id?: string;
inputId?: string;
labelId?: string;
menuId?: string;
itemCount?: number
}
I want to make a getInitialValue function with the intended signature –
// this has to be done correctly using generics, not been able to do it, read on…
(
props: Props extends AutosuggestProps,
stateKey: StateKey extends keyof AutosuggestState
) => AutosuggestState[StateKey] // return the correct type of AutosuggestState property based on which stateKey was passed
The behaviour of getInitialValue looks like this
// javascript version
const defaultStateValues = {
highlightedIndex: -1,
isOpen: false,
selectedItem: null,
inputValue: ''
}
function getDefaultValue(props, statePropKey) {
const defaultPropKey = `default${capitalizeString(statePropKey)}`
if (defaultPropKey in props) {
return props[defaultPropKey]
}
return defaultStateValues[statePropKey]
}
function getInitialValue(props, statePropKey) {
if (statePropKey in props) {
return props[statePropKey]
}
const initialPropKey = `initial${capitalizeString(statePropKey)}`
if (initialPropKey in props) {
return props[initialPropKey]
}
return getDefaultValue(props, statePropKey)
}
I'm finding it hard to write types of both getInitialValue and getDefaultValue such that getInitialValue correctly infers the right type as below –
const selectedItem = getInitialValue(props, 'selectedItem') // selectedItem variable should correctly be inferred as **object | null | string** since that's what its type is in **AutosuggestState** interface
Can someone help me write the types?
Here's a possible typing, which might or might not fit your use case:
function getDefaultValue<P extends AutosuggestProps, K extends keyof AutosuggestState>(
props: P,
statePropKey: K) {
const defaultPropKey = `default${capitalizeString(statePropKey)}`
if (defaultPropKey in props) {
return props[defaultPropKey as K] // assert here
}
return defaultStateValues[statePropKey]
}
function getInitialValue<P extends AutosuggestProps, K extends keyof AutosuggestState>(
props: P, statePropKey: K) {
if (statePropKey in props) {
return props[statePropKey]
}
const initialPropKey = `initial${capitalizeString(statePropKey)}`
if (initialPropKey in props) {
return props[initialPropKey as K] // assert here
}
return getDefaultValue(props, statePropKey)
}
All I've really done is make the functions generic in P and K of the types you alluded to in your pseudocode.
One decision you need to make is what to do when you use defaultPropKey and initialPropKey. The compiler has no way to verify that prepending "default" or "initial" to a property name will, if it exists, pick out another property of the same type. The compiler does not understand string concatenation at the type level (the suggestion for doing so in Github seems to be microsoft/TypeScript#12754). If you could refactor your interfaces so that the default and initial values are stored in properties named default and initial which are themselves objects holding properties of the same keys, then you could make the compiler figure it out. Assuming that you're keeping it this way, I've used a type assertion to say, for example, "treat initialPropKey as if it were of type K". Then the compiler will assume that props[initialPropKey] is of the same type as props[statePropKey].
Anyway, now the return types of those functions are inferred as
P[K] | {
highlightedIndex: number;
isOpen: boolean;
selectedItem: null;
inputValue: string;
}[K]
which makes sense since it uses defaultStateValues at the end, which is a concrete type not known to be the extended P type. This might be tightened up by using a conditional return type, but your example code didn't show a need for that.
Let's see if it works:
declare const props: AutosuggestProps;
const selectedItem = getInitialValue(props, 'selectedItem');
// const selectedItem: ItemDefaultType
Looks reasonable to me. Okay, hope that helps; good luck!
Link to code
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.