I am having trouble with setting proper types for react children.
export const recursiveCloneChildren = (
children: React.ReactNode,
handleChildChange,
disableContent: boolean,
) => {
return React.Children.map(children, (child) => {
if (!isObject(child)) {
return child;
}
let childProps = {
...child.props,
disabled: child.props.disabled || disableContent,
};
const requiredOrValidatableChildProps = {
...childProps,
checkValidationState: handleChildChange,
};
if (child.props.required || child.props.validatable) {
childProps = {
...requiredOrValidatableChildProps,
};
}
if (child.props.children) {
childProps.children = recursiveCloneChildren(child.props.children, handleChildChange, disableContent);
}
return React.cloneElement(child, childProps);
});
};
I am getting this error
Property 'props' does not exist on type '{} | ReactElement<any, string
| JSXElementConstructor> | ReactNodeArray | ReactPortal'.
Property 'props' does not exist on type '{}'.
I tried to set types for child directly ( React.ReactChild ), and showed another error on children.
How can it be solved?
I don't really like what you're trying to do, because if I found it in a codebase I'd be really confused.
But to answer your question. The child argument in the function passed to Children.map is of type ReactNode. This is the most general type, meaning you can end up with pretty much anything that can be a valid child. I actually don't know if your code is going to work but considering you want to access props, I'm assuming what you want is to make sure you're operating on ReactElement. In that case, your isObject check is not exhaustive enough. What you need is a type guard.
So the quick and dirty option is to write your own type guard and something like this is actually enough:
function isReactElement(child: React.ReactNode): child is React.ReactElement {
return isObject(child) && 'props' in child;
}
and then instead of
if (!isObject(child)) {
return child;
}
you just do
if (!isReactElement(child)) {
return child;
}
and it works. However probably a better idea is to use react-is library which is an official, Facebook-maintained library and it's widely used in many React libraries and it does basically exactly what you want but the checks are significantly better than what I proposed above. In your case you'd want to use isElement, the same way I showed above. Just make sure to install #types/react-is, too.
Related
TFlavour is a discriminatory union, which is then a value of an Object.
Trying to implement this, works in js, but ts gets angry.
ts playground link
Expected:
ts to understand discriminated unions in loops,
as it understands it without loops
type TFlavour = ({
natural: true,
naturalComponent : string
}) | ({
natural: false,
artificialComponent: string
})
type TIceCream = Record<string, TFlavour>
const IceCreams: TIceCream = {
CokeIceCream: {
natural:false,
artificialComponent: 'Coke'
},
Vanilla: {
natural: true,
naturalComponent: 'Vanilla Extract'
},
Mango: {
natural: true,
naturalComponent: 'Mango Fruit'
}
}
const iceCreamKeys = Object.keys(IceCreams)
iceCreamKeys.forEach( item => {
if(IceCreams[item].natural){
console.log(IceCreams[item].naturalComponent) // ts says "Property doesn't exists.."
}
})
if(IceCreams.Mango.natural){
console.log(IceCreams.Mango.naturalComponent) // Works here
}
The problem is that the compiler doesn't know how to do narrowing on an object property like IceCreams[item] where you are indexing with a key whose type isn't known to be a specific literal type. TypeScript is only following the type of the index, not the identity. And the type of item is string. If you have item1 and item2, both of type string, then checking IceCreams[item1] wouldn't let you conclude anything about IceCreams[item2], right? And since TypeScript can't tell the difference between item1 vs item2 or item vs item, it can't narrow. This is a known limitation of TypeScript reported at microsoft/TypeScript#10530. Maybe someday it will be addressed. But for now, there's an easy workaround:
Just copy the value into a new variable, so that the problematic indexing occurs only once:
iceCreamKeys.forEach(item => {
const x = IceCreams[item];
if (x.natural) {
console.log(x.naturalComponent) // okay
}
})
Playground link to code
Instead of directly accessing the item with the index, try to store it in a separate variable. This way, TypeScript will recognize the right type:
iceCreamKeys.forEach( item => {
const c = IceCreams[item]
if(c.natural){
console.log(c.naturalComponent)
}
})
(working TS Playground)
iceCreamKeys.forEach( item => {
if(IceCreams.item.natural){
console.log(IceCreams.item.naturalComponent) // Accessing it like this worked
}
})
Just found out, this works too.
My customer wants to have other instances of the app with another content as well (so called 'themes'). We decided to use env variable + prefilled object with content. So, this content may exist or it may not.
I created a function that conditionally imports module with all the content and it actually even working:
theme: string;
hasAnyTheme: boolean;
static getThemedMiscContent(): {[key: string]: string} {
let miscContent = {};
if (hasAnyTheme) {
import(`#/themes/customers-themes/${theme}/ThemedContent`)
.then(themedContent => {
miscContent = Object.assign(miscContent, themedContent.ThemedMiscContent);
});
}
return miscContent;
}
But when I call it from the component, I can't actually read properties of the object while I can read the object itself.
// computed
miscThemedContent(): {[key: string]: string} {
return ThemeUtil.getThemedMiscContent();
}
// template
{{ miscThemedContent }} // is totally fine
{{ miscThemedContent.someValue }} // undefined
And as the weird fact, this issue appears in only one of my 3 components that use that function. Others work just fine.
As far, as I understood, that kind of error appears when Vue tries to use that value before the object is loaded. So, I tried to add additional loading variable and nothing is happening. Is there any way to fix that?
Since import is an async function, miscContent could be returned before the import function's execution is completed. I suggest you make use of async/await syntax to wait for the actual result before returning the miscContent, it should be something like this:
static async getThemedMiscContent(): {[key: string]: string} {
let miscContent = {};
if (hasAnyTheme) {
const importTheme = await import(`#/themes/customers-themes/${theme}/ThemedContent`);
if (importTheme && importTheme.ThemedMiscContent) {
miscContent = Object.assign(miscContent, importTheme.ThemedMiscContent);
}
}
return miscContent;
}
Extending from this question React Set State For Nested Object
The way to update nested state is to decompose the object and re-construct it like the following:
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
However, this is a problem for me, as it will not preserve the component class (More detail below). I am aiming for my state to be structured like the following:
this.state = {
addressComponent: {
street: '',
number: '',
country: ''
},
weather: {
/* some other state*/
}
}
To make my life easier, I created a simple address class that builds the object for me and have some other utility function such as validation.
class AddressComponent {
constructor(street, number, country) {
this.street = street;
this.number = number;
this.country = country;
}
validate() {
if (this.street && this.number, this.country) {
return true;
}
return false;
}
}
This allows me to do is change the initialization of state to be:
this.state = {
addressComponent : new AddressComponent(),
weather: new WeatherComponent();
}
this will allow my view component to perform nice things like
if (this.state.addressComponent.validate()) {
// send this state to parent container
}
The problem with this approach is that if I want to mutate one single piece of information like country and use the above Stack Overflow approach such as:
this.setState({addressComponent: {...addressComponent, country: 'bluh'}})
Doing this will mean that the resolting addressComponent is no longer part of AddressComponent class hence no validate() function
To get around it I cound recreate a new AddressComponent class every time like:
this.setState({addressComponent: new AddressComponent(this.state.street, this.state.number, 'bluh');
But this seems weird.
Am I doing this wrong? Is there a better approach? Is it acceptable to use classes like this with react?
It's undesirable to use anything but plain objects for React state to avoid situations like this one. Using class instances will also make serialization and deserialization of the state much more complicated.
The existence of AddressComponent isn't justified, it doesn't benefit from being a class.
The same code could be rewritten as functional with plain objects:
const validateAddress = address => !!(street && address.number && address.country);
...
if (validateAddress(this.state.address)) {
// send this state to parent container
}
I think what you said about reinstantiating your class each time you update it in your state is the cleanest way to ensure your validate() method is called at that time. If it were me, I would probably write it as:
const { addressComponent } = this.state;
this.setState({ addressComponent: new AddressComponent(addressComponent.street, addressComponent.number, 'bluh') });
You can create a reusable function to update a complex state like this.
updateState = (option, value) => {
this.setState(prevState => ({
addressComponent: {
...prevState.addressComponent,
[option]: value
}
})
}
I have some dynamically generated function names in TypeScript. The only way I can use them now is to cast my objects to <any>. Ex.: <any>myInstance.getDataA(). These functions are dynamically generated based on some rules. Based on the same rules I'd like to generate type-definitions for my classes, but I can not make it work.
original.ts
abstract class Original {
dynamics = ['getData', 'setData'];
constructor() {
// I create functions here dynamically
this.dynamics.forEach((key) => {
this[key + this.info] = () => null;
});
}
get info() {
return 'X';
}
}
my-class.ts
class MyClass extends Original {
get info() {
return 'A';
}
}
my-other-class.ts
class MyOtherClass extends Original {
get info() {
return 'B';
}
}
something.ts
const myInstance = new MyClass();
console.log(myInstance.getDataA()); // TS2339: Property getDataA does not exist on type: 'MyClass'
const myOtherInstance = new MyOtherClass();
console.log(myInstance.getDataB()); // TS2339: Property getDataB does not exist on type: 'MyClass'
I would like to automatically generate a definition file to define these dynamic properties.
Ex.:
my-class.def.ts
declare interface MyClass {
getDataA;
setDataA
}
//my-other-class.def.ts
declare interface MyClass {
getDataB;
setDataB
}
But I can not find a syntax for my definition files to make it work. Pls ask me if I was not clear, and pls help if you have any idea!
Edit for 4.1
Using Template literal types and mapped type 'as' clauses we can now do concatenate strings in the type system and create a class that has these properties created dynamically.
function defineDynamicClass<T extends string[]>(...info: T): {
new (): {
[K in T[number] as `get${Capitalize<K>}`]: () => unknown
} & {
[K in T[number] as `set${Capitalize<K>}`]: (value: unknown) => void
} & {
info: T
}
} {
return class {
get info () {
return info;
}
} as any
}
class MyClass extends defineDynamicClass('A', 'B', 'ABAB') {
}
let s =new MyClass();
s.getA();
s.getABAB();
s.setA("")
s.info;
Playground Link
Before 4.1
The within language approach
There is no way to do this within the type system, since we can't perform string manipulation on string literal types. The closest you can get, without external tools, is to create get/set methods that take a string literal type, that will be of the same as that returned by the getInfo method.
function stringLiteralArray<T extends string>(...v: T[]){ return v;}
abstract class Original {
get(name: this['info'][number]) {
return null;
}
set(name: this['info'][number], value: any) {
return null;
}
get info() : string[]{
return [];
}
}
class MyOtherClass extends Original {
get info() {
return stringLiteralArray('A', 'B', 'ABAB');
}
}
class MyClass extends Original {
get info() {
return stringLiteralArray('C', 'D', 'DEDE');
}
}
let s =new MyClass();
s.get('A') // error
s.get('C') // ok
While this approach is not 100% what you want, form our previous discussions the aim was to have full code-completion for the methods, and this approach achieves this. You get errors if you pass in the wrong value and you get a completion list for the string:
The compiler API approach
A second approach would be to create a custom tool that uses the typescript compiler API to parse the ts files, look for classes derived from Original and generates interfaces containing the methods (either in the same file or a different file) , if you are interested in this I can write the code, but it's not trivial, and while the compiler API is stable I don't think the compiler team takes as much care with backward compatibility as they do with the language (in fact this is the exact statement they make in the documentation page).
If you are interested in such a solution, let me know and I can provide it, but I advise against it.
Please see the code below. I have to do two casting to avoid any flow error. If I use the commented out lines instead, it complains.
playground
/* #flow */
import * as React from "react";
type ConfObj = { label: string };
type Conf = React.Node | ConfObj;
type MyComponentProp = {
confs: Array<Conf>,
}
export default function MyComponent({
confs = [],
}: MyComponentProp) {
const items = confs.map((item, idx) => {
if (React.isValidElement(item)) {
// return React.cloneElement(item, {
return React.cloneElement(((item: any): React.Element<*>), {
key: idx.toString(),
});
}
const item2 = ((item: any): ConfObj);
return <span>{item2.label}</span>;
// return <span>{item.label}</span>;
});
return <div>items</div>
}
Is there a better way to do this to avoid the casting. Is there a better way to write isValidElement, so flow can deduce the type once the if condition matches. For example, if it is a valid react element, why do I need to cast it? or if it not, why accessing label gives error?
An item is of type Conf (which is Node | ConfObj)
When you enter the if statement, flow doesn't know for sure that item is a valid Element<*> (this could be known by flow I think tho), so you have to explicitly typecast it.
The <span>{item.label}</span> has the same problem. You also have to explicitly typecast it to a ConfObj, because a Node doesn't have a label attribute.