Next technique is extremely simple and useful.
Let's say you have next React component:
interface Props {
nameA: string;
nameB?: string;
}
const Component: React.FC<Props> = (props) => {
const { nameA, nameB } = props
const name = nameB || nameA;
return <div>Hello World! Name: {name}</div>
}
How to make nameB
mandatory if we don't pass nameA
?
To make it possible, we can use union
type.
interface Props1 {
nameA: string;
nameB?: string;
}
interface Props2 {
nameB: string;
}
type Props = Props1 | Props2
It is looks like it works and you can finish your reading...
Try to get nameA
property
const Comp: React.VFC<Props> = (props) => {
if(props.nameA){} // error
}
Bang! Error! But why???
This is how TS unions works. To fix it, and make it 99% type safe, we can add typeguards.
import React from 'react';
interface Props1 {
nameA: string;
nameB?: string;
}
interface Props2 {
nameB: string;
}
type Props = Props1 | Props2
const hasProperty = <T extends object>(obj: T, prop: string) =>
Object.hasOwnProperty.call(obj, prop);
const isA = (props: Props): props is Props1 =>
hasProperty(props,'nameA');
const isB = (props: Props): props is Props2 =>
!hasProperty(props,'nameA') && hasProperty(props,'nameB');
const Comp: React.VFC<Props> = (props) => {
if(isA(props)){
const y = props; // Props1
}
if(isB(props)){
const y = props; // Props2
}
return null
}
const result1 = <Comp nameB="b" /> // ok
const result2 = <Comp nameA="a" /> // ok
// error, 'nameB' is missing in type '{}' but required in type 'Props2'
const result3 = <Comp />
However, there are alternative ways.
This one, is the most common. You just need to add same non-optional property to bothProps1
and Props2
. Same technique is used for typing reduxactions
import React from 'react';
interface Props1 {
type:'1'
nameA: string;
nameB?: string;
}
interface Props2 {
type:'2'
nameB: string;
}
type Props = Props1 | Props2
const Comp: React.VFC<Props> = (props) => {
if(props.type==='1'){
const x = props; // Props1
}
if(props.type==='2'){
const x = props; // Props2
}
return null
}
const result1 = <Comp nameB="b" type="2"/> // ok
const result2 = <Comp nameA="a" type="1"/> // ok
// error, 'nameB' is missing in type '{}' but required in type 'Props2'
const result3 = <Comp />
Here you have another good example which involves algebraic data types definition/explanation.
// credits https://dev.to/gcanti/functional-design-algebraic-data-types-36kf
const enum Messages {
Success = 'Success',
Failure = 'Failure'
}
enum PromiseState {
Pending = 'Pending',
Fulfilled = 'Fulfilled',
Rejected = 'Rejected',
}
/**
* Let's assume we have React state,
* which implements all of these enums
* and one extra valid property
*
* Our state can have exact two variants (states)
*
* So how would you write this state?
*
* The first approach:
*/
/**
* This is the worst interface we can write for
* this particular case
* @product type from algebraic point of view
*
* @question : How many allowed states here can be?
* @answer : boolean(2) x Messages(2) x State(3) = 12
*
* @conclusion : more error prone
*/
interface ReactState {
valid: boolean;
error: Messages;
state: PromiseState;
}
/**
* @question : Should we allow such kind of state ?
* @answer : No!
*/
const thisState: ReactState = {
valid: true,
error: Messages.Failure,
state: PromiseState.Pending,
}
/**
* Much better way.
* @question : How many state I should allow?
* @answer : 2
*
*/
interface Failure {
valid: false;
error: Messages.Failure;
state: PromiseState.Rejected
}
interface Success {
valid: true;
error: Messages.Success;
state: PromiseState.Fulfilled
}
/**
* @SUM type from @algebraic point of view
* @UNION type from @TypeScript point of view
*
* @question : How many allowed states can be here?
* @answer : 2
*
* @conclusion : less error prone
*/
export type ResponseState = Failure | Success;
// Try to change some property
const result0: ResponseState = {
valid: true,
error: Messages.Success,
state: PromiseState.Fulfilled
}
const handleState = (state: ResponseState) => {
if (state.valid === true) {
state // Success
} else {
state; //Failure
}
}
/**
* @summary
* Please use @product types when your properties are self independent
* and @sum when they are not
*/
You can also use nextutilfor unionizing
// credits goes to Titian Cernicova-Dragomir
import React from 'react';
interface Props1 {
nameA: string;
nameB?: string;
}
interface Props2 {
nameB: string;
}
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type Props = StrictUnion<Props1 | Props2>
With help of StrictUnion
you can create more stricter union types. I have found this utility very useful.
Don't forget, that we have a type util for checking whether type is union or not
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;