Skip to content

Commit

Permalink
[Card] Fix TypeScript not recognizing "component" prop (#20179)
Browse files Browse the repository at this point in the history
  • Loading branch information
rart authored Mar 30, 2020
1 parent 4573dcf commit 92a6d92
Show file tree
Hide file tree
Showing 2 changed files with 357 additions and 9 deletions.
67 changes: 58 additions & 9 deletions packages/material-ui/src/CardHeader/CardHeader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,28 @@ import * as React from 'react';
import { TypographyProps } from '../Typography';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';

export interface CardHeaderTypeMap<P = {}, D extends React.ElementType = 'div'> {
props: P & {
export interface CardHeaderTypeMap<
Props = {},
DefaultComponent extends React.ElementType = 'div',
TitleTypographyComponent extends React.ElementType = 'span',
SubheaderTypographyComponent extends React.ElementType = 'span'
> {
props: Props & {
action?: React.ReactNode;
avatar?: React.ReactNode;
disableTypography?: boolean;
subheader?: React.ReactNode;
subheaderTypographyProps?: Partial<TypographyProps>;
subheaderTypographyProps?: TypographyProps<
SubheaderTypographyComponent,
{ component?: SubheaderTypographyComponent }
>;
title?: React.ReactNode;
titleTypographyProps?: Partial<TypographyProps>;
titleTypographyProps?: TypographyProps<
TitleTypographyComponent,
{ component?: TitleTypographyComponent }
>;
};
defaultComponent: D;
defaultComponent: DefaultComponent;
classKey: CardHeaderClassKey;
}
/**
Expand All @@ -25,13 +36,51 @@ export interface CardHeaderTypeMap<P = {}, D extends React.ElementType = 'div'>
*
* - [CardHeader API](https://material-ui.com/api/card-header/)
*/
declare const CardHeader: OverridableComponent<CardHeaderTypeMap>;
declare const CardHeader: OverridableCardHeader;

export interface OverridableCardHeader extends OverridableComponent<CardHeaderTypeMap> {
<
DefaultComponent extends React.ElementType = CardHeaderTypeMap['defaultComponent'],
Props = {},
TitleTypographyComponent extends React.ElementType = 'span',
SubheaderTypographyComponent extends React.ElementType = 'span'
>(
props: CardHeaderPropsWithComponent<
DefaultComponent,
Props,
TitleTypographyComponent,
SubheaderTypographyComponent
>,
): JSX.Element;
}

export type CardHeaderClassKey = 'root' | 'avatar' | 'action' | 'content' | 'title' | 'subheader';

export type CardHeaderProps<
D extends React.ElementType = CardHeaderTypeMap['defaultComponent'],
P = {}
> = OverrideProps<CardHeaderTypeMap<P, D>, D>;
DefaultComponent extends React.ElementType = CardHeaderTypeMap['defaultComponent'],
Props = {},
TitleTypographyComponent extends React.ElementType = 'span',
SubheaderTypographyComponent extends React.ElementType = 'span'
> = OverrideProps<
CardHeaderTypeMap<
Props,
DefaultComponent,
TitleTypographyComponent,
SubheaderTypographyComponent
>,
DefaultComponent
>;

export type CardHeaderPropsWithComponent<
DefaultComponent extends React.ElementType = CardHeaderTypeMap['defaultComponent'],
Props = {},
TitleTypographyComponent extends React.ElementType = 'span',
SubheaderTypographyComponent extends React.ElementType = 'span'
> = { component?: DefaultComponent } & CardHeaderProps<
DefaultComponent,
Props,
TitleTypographyComponent,
SubheaderTypographyComponent
>;

export default CardHeader;
299 changes: 299 additions & 0 deletions packages/material-ui/src/CardHeader/CardHeader.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import * as React from 'react';
import CardHeader, { CardHeaderProps, CardHeaderTypeMap } from '@material-ui/core/CardHeader';

const CustomComponent: React.FC<{ stringProp: string; numberProp: number }> = () => <div />;

type DefaultComponent = CardHeaderTypeMap['defaultComponent'];

interface ComponentProp {
component?: React.ElementType;
}

function createElementBasePropMixedTest() {
React.createElement<CardHeaderProps<DefaultComponent, ComponentProp>>(CardHeader);
React.createElement<CardHeaderProps<DefaultComponent, ComponentProp>>(CardHeader, {
component: 'div',
});
// ExpectError: type system should be demanding the required props of "CustomComponent"
React.createElement<CardHeaderProps<DefaultComponent, ComponentProp>>(CardHeader, {
component: CustomComponent,
});
// $ExpectError
React.createElement<CardHeaderProps<DefaultComponent, ComponentProp>>(CardHeader, {
// This test shouldn't fail but does; stringProp & numberProp are required props of CustomComponent
component: CustomComponent,
stringProp: '',
numberProp: 0,
});
React.createElement<CardHeaderProps>(CardHeader, {
disableTypography: true,
});
// $ExpectError
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
unknownProp: 'shouldNotWork',
});
// $ExpectError
React.createElement<CardHeaderProps>(CardHeader, {
disableTypography: 'hello',
});
// $ExpectError
React.createElement<CardHeaderProps>(CardHeader, {
disableTypography: 1,
});
// $ExpectError
React.createElement<CardHeaderProps<any, ComponentProp>>(CardHeader, {
component: 'incorrectElement',
});
}

function createElementTypographyTest() {
React.createElement<CardHeaderProps>(CardHeader, {
titleTypographyProps: {
align: 'center',
},
});
// $ExpectError
React.createElement<CardHeaderProps>(CardHeader, {
titleTypographyProps: {
align: 'incorrectAlign',
},
});
React.createElement<CardHeaderProps>(CardHeader, {
titleTypographyProps: {
variant: 'body1',
},
});
// $ExpectError
React.createElement<CardHeaderProps>(CardHeader, {
titleTypographyProps: {
variant: 123,
},
});
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
component: 'div',
},
});
// ExpectError: This is expected to err; the type system should catch required props from "CustomComponent".
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
component: CustomComponent,
},
});
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
component: CustomComponent,
stringProp: '',
numberProp: 0,
},
});
// ExpectError: This is expected to err; the type system should catch the props type mismatch
// from "CustomComponent" props.
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
component: CustomComponent,
stringProp: 0,
numberProp: '',
},
});
// ExpectError: This is expected to err; the type system is welcoming unknown props.
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
unknownProp: 'shouldNotWork',
},
});
// $ExpectError
React.createElement<CardHeaderProps<DefaultComponent, {}, React.ElementType>>(CardHeader, {
titleTypographyProps: {
component: 'incorrectComponent',
},
});
// $ExpectError
React.createElement<CardHeaderProps>(CardHeader, {
titleTypographyProps: true,
});
}

function componentPropTest() {
<CardHeader component="div" />;
<CardHeader component={CustomComponent} stringProp="string" numberProp={1} />;
// $ExpectError
<CardHeader component="incorrectComponent" />;
// $ExpectError
<CardHeader component={CustomComponent} />;
}

function mixedCardHeaderComponentAndTypographyTest() {
<CardHeader component="div" titleTypographyProps={{ component: 'a', href: 'href' }} />;
<CardHeader component="div" subheaderTypographyProps={{ component: 'a', href: 'href' }} />;
<CardHeader
component={CustomComponent}
stringProp="string"
numberProp={1}
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp', numberProp: 2 }}
/>;
<CardHeader
component={CustomComponent}
stringProp="string"
numberProp={1}
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp', numberProp: 2 }}
subheaderTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: 2,
}}
/>;
// $ExpectError
<CardHeader component="incorrectComponent" />;
// $ExpectError
<CardHeader component={CustomComponent} />;
<CardHeader
component={CustomComponent}
stringProp="string"
numberProp={1}
// $ExpectError
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp' }}
subheaderTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: 2,
}}
/>;
// $ExpectError
<CardHeader
component={CustomComponent}
stringProp="string"
numberProp={1}
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp' }}
subheaderTypographyProps={{ component: CustomComponent, stringProp: 'stringProp' }}
/>;
<CardHeader
// $ExpectError
component="incorrectComponent"
stringProp="string"
numberProp={1}
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp', numberProp: 2 }}
subheaderTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: 2,
}}
/>;
}

function titleTypographyPropsTest() {
// $ExpectError
<CardHeader titleTypographyProps={{ component: 'incorrectComponent' }} />;
<CardHeader titleTypographyProps={{ component: 'a', href: 'href' }} />;
<CardHeader
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp', numberProp: 2 }}
/>;
<CardHeader titleTypographyProps={{ variant: 'h1' }} />;
<CardHeader titleTypographyProps={{ align: 'left' }} />;
<CardHeader
titleTypographyProps={{
color: 'primary',
display: 'block',
gutterBottom: true,
noWrap: true,
variantMapping: { h1: 'h1' },
}}
/>;
// $ExpectError
<CardHeader
titleTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: '',
}}
/>;
// $ExpectError
<CardHeader titleTypographyProps={{ component: CustomComponent, numberProp: 2 }} />;
<CardHeader
titleTypographyProps={{
component: 'a',
// ExpectError: This is expected to err; the type system is welcoming unknown props.
propThatDoesntExist: 'shouldNotWork',
}}
/>;
}

function subheaderTypographyPropsTest() {
<CardHeader subheaderTypographyProps={{ component: 'a', href: 'href' }} />;
<CardHeader
subheaderTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: 2,
}}
/>;
<CardHeader subheaderTypographyProps={{ variant: 'h1' }} />;
<CardHeader subheaderTypographyProps={{ align: 'left' }} />;
<CardHeader
subheaderTypographyProps={{
color: 'primary',
display: 'block',
gutterBottom: true,
noWrap: true,
variantMapping: { h1: 'h1' },
}}
/>;
<CardHeader
subheaderTypographyProps={{
component: 'a',
// ExpectError: This is expected to err; the type system is welcoming unknown props.
propThatDoesntExist: 'shouldNotWork',
}}
/>;
// $ExpectError
<CardHeader subheaderTypographyProps={{ component: 'incorrectComponent' }} />;
// $ExpectError
<CardHeader subheaderTypographyProps={{ component: CustomComponent, numberProp: 2 }} />;
}

function mixedTypographyPropsTest() {
<CardHeader
titleTypographyProps={{ component: 'a', href: 'href' }}
subheaderTypographyProps={{ component: 'a', href: 'href' }}
/>;
<CardHeader
titleTypographyProps={{ component: CustomComponent, stringProp: 'stringProp', numberProp: 2 }}
subheaderTypographyProps={{
component: CustomComponent,
stringProp: 'stringProp',
numberProp: 2,
}}
/>;
// $ExpectError
<CardHeader
titleTypographyProps={{ component: 'incorrectComponent' }}
subheaderTypographyProps={{ component: 'incorrectComponent' }}
/>;
<CardHeader
titleTypographyProps={{
component: 'a',
// ExpectError: This is expected to err; the type system is welcoming unknown props.
propThatDoesntExist: 'shouldNotWork',
}}
subheaderTypographyProps={{
component: 'a',
// ExpectError: This is expected to err; the type system is welcoming unknown props.
propThatDoesntExist: 'shouldNotWork',
}}
/>;
// $ExpectError
<CardHeader
titleTypographyProps={{ component: CustomComponent, numberProp: 2 }}
subheaderTypographyProps={{ component: CustomComponent, numberProp: 2 }}
/>;
<CardHeader
// $ExpectError
titleTypographyProps={{ component: CustomComponent, numberProp: 2 }}
subheaderTypographyProps={{ component: CustomComponent, numberProp: 2, stringProp: 'yada' }}
/>;
<CardHeader
titleTypographyProps={{ component: CustomComponent, numberProp: 2, stringProp: 'yada' }}
// $ExpectError
subheaderTypographyProps={{ component: CustomComponent, numberProp: 2 }}
/>;
}

0 comments on commit 92a6d92

Please sign in to comment.