Reputation: 3142
In a React project I am using a type guard to ensure I am passing down a prop that has the appropriate type. This seems to work because TypeScript is not raising any error in my IDE. But when running type-check as a command, the compiler fails on that prop.
I have a component that is rendering a child component. That child component needs to receive a prop with a custom type, declared in an enum AuthorizedType
. To ensure that the prop passed is valid, I created a type guard isAuthorizedType
. If the type is not valid, the component won't render.
Because the child component is not rendered when the type guard is failing, TypeScript is not raising any error in my IDE. But when running type-check as a command, the compiler raises the error:
TS2322: Type 'string' is not assignable to type 'AuthorizedType'.
I wonder why is the error not raised in the IDE but raised when running type-checking. I understand that type guards perform a runtime check, and I wonder what it means exactly and if that's related.
Here is the code:
// ParentComponent.tsx
export enum AuthorizedType {
First = 'first',
Second = 'second',
}
function isAuthorizedType(value: any): value is AuthorizedType {
return Object.values(AuthorizedType).indexOf(value) !== -1;
}
function ParentComponent({value}: {value: string}) {
const isValidType = isAuthorizedType(value);
if (!isValidType) {
return null;
}
return (<ChildComponent value={value} />)
}
// ChildComponent.tsx
import {AuthorizedType} from '..';
function ChildComponent({value}: {value: AuthorizedType}) {
switch(value) {
case AuthorizedType.First:
return <div>first</div>
case AuthorizedType.Second:
return <div>second</div>
}
}
By moving the type guard into the child component, type-check is not raising any error anymore:
// ChildComponent.tsx
import {AuthorizedType} from '..';
function isAuthorizedType(value: any): value is AuthorizedType {
return Object.values(AuthorizedType).indexOf(value) !== -1;
}
function ChildComponent({value}: {value: string}) {
const isValidType = isAuthorizedType(value);
if (!isValidType) {
return null;
}
switch(value) {
case AuthorizedType.First:
return <div>first</div>
case AuthorizedType.Second:
return <div>second</div>
}
}
I would like to understand why.
Upvotes: 1
Views: 1312
Reputation: 855
After looking through your edits I thought it best to write up another answer entirely. Your program logic doesn't seem flawed to me and after verifying it, so I believe you've found a bug in TypeScript's control flow analysis.
Here's a playground link that works as expected on TS versions equal or below 4.3.5
but not as expected on versions equal or above 4.4.4
. On the buggy version (>=4.4.4
), the compiler reckons the return type of ParentComponent
to be null
, but actually calling the emitted ParentComponent()
function returns "first"
.
I might've of course missed a configuration flag or something that affects how CFA works, or there might've been an intentional change in version 4.4.x, so check if you can find a related bug in GitHub. If not, I think you should file a report about this.
Upvotes: 1
Reputation: 855
Your ChildComponent
is marked to expect an AuthorizedType
as props
. However in ParentComponent
you're calling the child with { prop: AuthorizedType }
as props
. AuthorizedType
extends string
and that cannot overlap with the type { prop: AuthorizedType }
and thus you get a TS2322 error.
The reason it goes away in your situation is when you move the isAuthorizedType
call into your child component, it's trying to check if the props
value it receives (i.e. { prop: AuthorizedType }
) is an AuthorizedType
(which it cannot be, since object
does not extend string
), assigns false
to isValidType
, and renders null
.
So the problem is caused by expecting AuthorizedType
as props
in ChildComponent
when actually you should expect { prop: AuthorizedType }
. Of course, you'll probably want to name the property with something more descriptive like "value":
function Child(props: { value: AuthorizedType }) {
switch(prop.value) {
case AuthorizedType.First:
return <div>first</div>
case AuthorizedType.Second:
return <div>second</div>
}
}
The following format (using object destructuring) is equivalent to the above one but might prove more ergonomic:
interface ChildProps {
value: AuthorizedType
}
function Child({ value }: ChildProps) {
switch(value) {
case AuthorizedType.First:
return <div>first</div>
case AuthorizedType.Second:
return <div>second</div>
}
}
You'll then call the Child
with e.g. (see jsx ...
spread attributes):
function ParentComponent(props: ChildProps) {
return isAuthorizedType(props.value)
? <Child {...props} />
: null;
}
Upvotes: 1