27
loading...
This website collects cookies to deliver better user experience
A
, B
and C
.enum Mode {
easy = 'easy',
medium = 'medium',
hard = 'hard'
}
type A = {
mode: Mode.easy;
data: string;
check: (a: A['data']) => string
}
type B = {
mode: Mode.medium;
data: number;
check: (a: B['data']) => number
}
type C = {
mode: Mode.hard;
data: number[];
check: (a: C['data']) => number
}
type Props = A | B | C;
const Comp: FC<Props> = (props) => {
if (props.mode === Mode.easy) {
const x = props // A
}
if (props.mode === Mode.medium) {
const x = props // B
}
if (props.mode === Mode.hard) {
const x = props // C
}
return null
}
props.check
outside of condition statement.const Comp: FC<Props> = (props) => {
props.check(props.data) // error
return null
}
type Intersection = string & number & number[] // never
check
expects never
type.const Comp: FC<Props> = ({ check, data, mode }) => {
if (mode === Mode.easy) {
check(data) // error
}
return null
}
const isEasy = <M extends Mode>(
mode: M,
check: Fn
): check is Extract<Props, { mode: Mode.easy }>['check'] =>
mode === Mode.easy
js
to ts
.check
we need to overload it.// Get keys where value is a function
type FnProps<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// check
type Result0 = FnProps<Props>
type Values<T> = T[keyof T]
type FnUnion<PropsUnion> = Values<Pick<PropsUnion, FnProps<PropsUnion>>>
// | ((a: A['data']) => string)
// | ((a: B['data']) => number)
// | ((a: C['data']) => number)
type Result1 = FnUnion<Props>
type ParametersUnion<PropsUnion> =
FnUnion<PropsUnion> extends Fn
? (a: Parameters<FnUnion<PropsUnion>>[0]) =>
ReturnType<FnUnion<PropsUnion>>
: never
// (a: string | number | number[]) => string | number
type Result2 = ParametersUnion<Props>
// 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;
type Overload<PropsUnion> =
& UnionToIntersection<PropsUnion[FnProps<PropsUnion>]>
& ParametersUnion<PropsUnion>
// & ((a: A['data']) => string)
// & ((a: B['data']) => number)
// & ((a: C['data']) => number)
// & ((a: string | number | number[]) => string | number)
type Result3 = Overload<Props>
type OverloadedProps<PropsUnion> =
& PropsUnion
& Record<FnProps<PropsUnion>, Overload<PropsUnion>>
// Props & Record<"check", Overload<Props>>
type Result4 = OverloadedProps<Props>
import React, { FC } from 'react'
enum Mode {
easy = 'easy',
medium = 'medium',
hard = 'hard'
}
type A = {
mode: Mode.easy;
data: string;
check: (a: A['data']) => string
}
type B = {
mode: Mode.medium;
data: number;
check: (a: B['data']) => number
}
type C = {
mode: Mode.hard;
data: number[];
check: (a: C['data']) => number
}
type Fn = (...args: any[]) => any
// 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;
type Props = A | B | C;
// Get keys where value is a function
type FnProps<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// check
type Result0 = FnProps<Props>
type Values<T> = T[keyof T]
type FnUnion<PropsUnion> = Values<Pick<PropsUnion, FnProps<PropsUnion>>>
// | ((a: A['data']) => string)
// | ((a: B['data']) => number)
// | ((a: C['data']) => number)
type Result1 = FnUnion<Props>
type ParametersUnion<PropsUnion> =
FnUnion<PropsUnion> extends Fn
? (a: Parameters<FnUnion<PropsUnion>>[0]) =>
ReturnType<FnUnion<PropsUnion>>
: never
// (a: string | number | number[]) => string | number
type Result2 = ParametersUnion<Props>
type Overload<PropsUnion> =
& UnionToIntersection<PropsUnion[FnProps<PropsUnion>]>
& ParametersUnion<PropsUnion>
// & ((a: A['data']) => string)
// & ((a: B['data']) => number)
// & ((a: C['data']) => number)
// & ((a: string | number | number[]) => string | number)
type Result3 = Overload<Props>
type OverloadedProps<PropsUnion> =
& PropsUnion
& Record<FnProps<PropsUnion>, Overload<PropsUnion>>
// Props & Record<"check", Overload<Props>>
type Result4 = OverloadedProps<Props>
const Comp: FC<OverloadedProps<Props>> = (props) => {
const { mode, data, check } = props;
if (props.mode === Mode.easy) {
props.data // string
}
const result = check(data) // string | number
return null
}
I have two components with similar props, but there is a crucial difference. One component, called TabsWithState
takes only a single prop tabs
, which is an array of objects of the following shape:
interface ItemWithState {
name: string
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
Another similar…
tabs
property is common:interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const Tabs = (props: TabsProps) => {
if (props.withRouter) { // error
return <TabsWithRouter {...props} />; // error
}
return <TabsWithState {...props} />; // error
};
withRouter
property, since it is optional. Instead, it allows you to get only common property which is tabs
. This is expected behavior.withRouter?:never
to our WithStateProps
type.{...props}
. But it has one small drawback: it allows us to pass to Tabs
component illegal props:import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
withRouter?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const Tabs = (props: TabsProps) => {
if (props.withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const Tabs = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
hacks
. Our WithStateProps
type should not have any extra optional props. But it still has the same drawback. Illegal state is allowed.// type Overload = FC<WithStateProps> & FC<WithRouterProps>
const Tabs: FC<WithStateProps> & FC<WithRouterProps> = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
// 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;
type Distributive<T> = T extends any ? FC<T> : never
type Overload = UnionToIntersection<Distributive<TabsProps>>
const Tabs: Overload = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
type Overloading =
& ((props: WithStateProps) => JSX.Element)
& ((props: WithRouterProps) => JSX.Element)
Animal
component with next constraints:dogName
is empty string or unset, canBark
should be falsedogName
is not empty string, canBark
should be true
type NonEmptyString<T extends string> = T extends '' ? never : T;
type WithName = {
dogName: string,
canBark: true,
}
type WithoutName = {
dogName?: '',
canBark: false
};
type Props = WithName | WithoutName;
type Overloadings =
& ((arg: { canBark: false }) => JSX.Element)
& ((arg: { dogName: '', canBark: false }) => JSX.Element)
& (<S extends string>(arg: { dogName: NonEmptyString<S>, canBark: true }) => JSX.Element)
const Animal: Overloadings = (props: Props) => {
return null as any
}
import React, { FC } from 'react'
type NonEmptyString<T extends string> = T extends '' ? never : T;
type WithName = {
dogName: string,
canBark: true,
}
type WithoutName = {
dogName?: '',
canBark: false
};
type Props = WithName | WithoutName;
type Overloadings =
& ((arg: { canBark: false }) => JSX.Element)
& ((arg: { dogName: '', canBark: false }) => JSX.Element)
& (<S extends string>(arg: { dogName: NonEmptyString<S>, canBark: true }) => JSX.Element)
const Animal: Overloadings = (props: Props) => {
return null as any
}
const Test = () => {
return (
<>
<Animal dogName='' canBark={false} /> // ok
<Animal dogName='a' canBark={true} /> // ok
<Animal canBark={false} /> // ok
<Animal dogName='a' canBark={false} /> // error
<Animal dogName='' canBark={true} /> // error
<Animal canBark={true} /> // error
</>
)
}
foo
and bar
properties to be strings, but property foo
can't be hello
.foo
and bar
property.import React from 'react'
type Props<F extends string = '', B extends string = ''> = {
foo: F;
bar: B;
}
type ConditionalProps<T> = T extends { foo: infer Foo; bar: string } ? Foo extends 'hello' ? never : T : never
const Example = <F extends string, B extends string>(props: ConditionalProps<Props<F, B>>) => {
return null as any
}
const Test = () => {
<>
<Example foo='hello' bar='bye' /> // expected error
<Example foo='not hello' bar='1' /> // ok
</>
}