Skip to content

Commit

Permalink
[core] Use custom $ExpectType assertion (#21309)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Jun 4, 2020
1 parent 76a6aa8 commit 9bd4277
Show file tree
Hide file tree
Showing 16 changed files with 132 additions and 100 deletions.
4 changes: 3 additions & 1 deletion packages/material-ui-lab/package.json
Expand Up @@ -51,7 +51,9 @@
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
},
"devDependencies": {},
"devDependencies": {
"@material-ui/types": "^5.1.0"
},
"sideEffects": false,
"publishConfig": {
"access": "public"
Expand Down
@@ -1,4 +1,5 @@
import { Autocomplete, AutocompleteProps } from '@material-ui/lab';
import { expectType } from '@material-ui/types';

interface MyAutocomplete<
T,
Expand All @@ -22,8 +23,7 @@ function MyAutocomplete<
<MyAutocomplete
options={['1', '2', '3']}
onChange={(event, value) => {
// $ExpectType string[]
value;
expectType<string[], typeof value>(value);
}}
renderInput={() => null}
multiple
Expand Down
@@ -1,4 +1,5 @@
import { useAutocomplete } from '@material-ui/lab';
import { useAutocomplete, FilterOptionsState } from '@material-ui/lab';
import { expectType } from '@material-ui/types';

interface Person {
id: string;
Expand All @@ -18,8 +19,7 @@ const persons: Person[] = [
useAutocomplete({
options: ['1', '2', '3'],
onChange(event, value) {
// $ExpectType string | null
value;
expectType<string | null, typeof value>(value);
},
});

Expand All @@ -28,46 +28,40 @@ useAutocomplete({
options: ['1', '2', '3'],
multiple: false,
onChange(event, value) {
// $ExpectType string | null
value;
expectType<string | null, typeof value>(value);
},
});

// value type is inferred correctly for type unions
useAutocomplete({
options: ['1', '2', '3', 4, true],
onChange(event, value) {
// $ExpectType string | number | boolean | null
value;
expectType<string | number | boolean | null, typeof value>(value);
},
});

// value type is inferred correctly for interface
useAutocomplete({
options: persons,
onChange(event, value) {
// $ExpectType Person | null
value;
expectType<Person | null, typeof value>(value);
},
});

// value type is inferred correctly when value is set
useAutocomplete({
options: ['1', '2', '3'],
onChange(event, value) {
// $ExpectType string | null
expectType<string | null, typeof value>(value);
value;
},
filterOptions(options, state) {
// $ExpectType FilterOptionsState<string>
state;
// $ExpectType string[]
options;
expectType<FilterOptionsState<string>, typeof state>(state);
expectType<string[], typeof options>(options);
return options;
},
getOptionLabel(option) {
// $ExpectType string
option;
expectType<string, typeof option>(option);
return option;
},
value: null,
Expand All @@ -80,7 +74,7 @@ useAutocomplete({
options: ['1', '2', '3'],
multiple: true,
onChange(event, value) {
// $ExpectType string[]
expectType<string[], typeof value>(value);
value;
},
});
Expand All @@ -90,8 +84,7 @@ useAutocomplete({
options: ['1', '2', '3', 4, true],
multiple: true,
onChange(event, value) {
// $ExpectType (string | number | boolean)[]
value;
expectType<Array<string | number | boolean>, typeof value>(value);
},
});

Expand All @@ -100,7 +93,7 @@ useAutocomplete({
options: persons,
multiple: true,
onChange(event, value) {
// $ExpectType Person[]
expectType<Person[], typeof value>(value);
value;
},
});
Expand All @@ -118,34 +111,30 @@ useAutocomplete({
options: ['1', '2', '3'],
disableClearable: true,
onChange(event, value) {
// $ExpectType string
value;
expectType<string, typeof value>(value);
},
});

useAutocomplete({
options: ['1', '2', '3'],
disableClearable: false,
onChange(event, value) {
// $ExpectType string | null
value;
expectType<string | null, typeof value>(value);
},
});

useAutocomplete({
options: ['1', '2', '3'],
onChange(event, value) {
// $ExpectType string | null
value;
expectType<string | null, typeof value>(value);
},
});

// Free solo
useAutocomplete({
options: persons,
onChange(event, value) {
// $ExpectType string | Person | null
value;
expectType<string | Person | null, typeof value>(value);
},
freeSolo: true,
});
Expand All @@ -154,8 +143,7 @@ useAutocomplete({
options: persons,
disableClearable: true,
onChange(event, value) {
// $ExpectType string | Person
value;
expectType<string | Person, typeof value>(value);
},
freeSolo: true,
});
Expand All @@ -164,8 +152,7 @@ useAutocomplete({
options: persons,
multiple: true,
onChange(event, value) {
// $ExpectType (string | Person)[]
value;
expectType<Array<string | Person>, typeof value>(value);
},
freeSolo: true,
});
@@ -1,5 +1,6 @@
import { useTheme, makeStyles, styled } from '@material-ui/styles';
import Grid from '@material-ui/core/Grid';
import { expectType } from '@material-ui/types';

declare module '@material-ui/styles' {
interface DefaultTheme {
Expand All @@ -8,14 +9,14 @@ declare module '@material-ui/styles' {
}

{
// $ExpectType string
const value = useTheme().myProperty;
expectType<string, typeof value>(value);
}

{
makeStyles((theme) => {
// $ExpectType string
const value = theme.myProperty;
expectType<string, typeof value>(value);

return {
root: {
Expand All @@ -27,8 +28,8 @@ declare module '@material-ui/styles' {

{
styled(Grid)(({ theme }) => {
// $ExpectType string
const value = theme.myProperty;
expectType<string, typeof value>(value);

return {
width: 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/material-ui-styles/src/withStyles/withStyles.d.ts
Expand Up @@ -9,7 +9,7 @@ export {};

type JSSFontface = CSS.FontFace & { fallbacks?: CSS.FontFace[] };

type PropsFunc<Props extends object, T> = (props: Props) => T;
export type PropsFunc<Props extends object, T> = (props: Props) => T;

/**
* Allows the user to augment the properties available
Expand Down
29 changes: 22 additions & 7 deletions packages/material-ui-styles/test/styles.spec.tsx
Expand Up @@ -6,9 +6,13 @@ import {
WithTheme,
WithStyles,
makeStyles,
CSSProperties,
CreateCSSProperties,
PropsFunc,
} from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import { Theme } from '@material-ui/core/styles';
import { expectType } from '@material-ui/types';

// Example 1
const styles = ({ palette, spacing }: Theme) => ({
Expand Down Expand Up @@ -449,38 +453,49 @@ function forwardRefTest() {
}));

const styles = useStyles({ foo: true });
// $ExpectType string
const root = styles.root;
// $ExpectType string
const root2 = styles.root2;
expectType<Record<'root' | 'root2', string>, typeof styles>(styles);
}

{
// If there are no props, use the definition that doesn't accept them
// https://github.com/mui-org/material-ui/issues/16198

// $ExpectType Record<"root", CSSProperties | CreateCSSProperties<{}> | PropsFunc<{}, CreateCSSProperties<{}>>>
const styles = createStyles({
root: {
width: 1,
},
});
expectType<
Record<'root', CSSProperties | CreateCSSProperties | PropsFunc<{}, CreateCSSProperties>>,
typeof styles
>(styles);

// $ExpectType Record<"root", CSSProperties | CreateCSSProperties<{}> | PropsFunc<{}, CreateCSSProperties<{}>>>
const styles2 = createStyles({
root: () => ({
width: 1,
}),
});
expectType<
Record<'root', CSSProperties | CreateCSSProperties | PropsFunc<{}, CreateCSSProperties>>,
typeof styles2
>(styles2);

interface testProps {
foo: boolean;
}

// $ExpectType Record<"root", CSSProperties | CreateCSSProperties<testProps> | PropsFunc<testProps, CreateCSSProperties<testProps>>>
const styles3 = createStyles({
root: (props: testProps) => ({
width: 1,
}),
});
expectType<
Record<
'root',
| CSSProperties
| CreateCSSProperties<testProps>
| PropsFunc<testProps, CreateCSSProperties<testProps>>
>,
typeof styles3
>(styles3);
}
21 changes: 21 additions & 0 deletions packages/material-ui-types/index.d.ts
Expand Up @@ -62,3 +62,24 @@ type GenerateStringUnion<T> = Extract<
}[keyof T],
string
>;

// https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same
type IfEquals<T, U, Y = unknown, N = never> = (<G>() => G extends T ? 1 : 2) extends <
G
>() => G extends U ? 1 : 2
? Y
: N;

/**
* Issues a type error if `Expected` is not identical to `Actual`.
*
* `Expected` should be declared when invoking `expectType`.
* `Actual` should almost always we be a `typeof value` statement.
*
* @example `expectType<number | string, typeof value>(value)`
* TypeScript issues a type error since `value is not assignable to never`.
* This means `typeof value` is not identical to `number | string`
*
* @param actual
*/
export function expectType<Expected, Actual>(actual: IfEquals<Actual, Expected, Actual>): void;
9 changes: 9 additions & 0 deletions packages/material-ui-types/index.spec.ts
@@ -0,0 +1,9 @@
import { expectType } from '@material-ui/types';

function expectTypeTypes() {
// it rejects assignability to `any`
function onClick(event: any) {
// @ts-expect-error
expectType<MouseEvent, typeof event>(event);
}
}
13 changes: 7 additions & 6 deletions packages/material-ui/src/Button/Button.spec.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import Button, { ButtonProps } from '@material-ui/core/Button';
import { Link as ReactRouterLink, LinkProps } from 'react-router-dom';
import { expectType } from '@material-ui/types';

const log = console.log;

Expand Down Expand Up @@ -37,10 +38,10 @@ const ButtonTest = () => (
// By default the underlying component is a button element:
<Button
ref={(elem) => {
elem; // $ExpectType HTMLButtonElement | null
expectType<HTMLButtonElement | null, typeof elem>(elem);
}}
onClick={(e) => {
e; // $ExpectType MouseEvent<HTMLButtonElement, MouseEvent>
expectType<React.MouseEvent<HTMLButtonElement, MouseEvent>, typeof e>(e);
log(e);
}}
>
Expand All @@ -50,10 +51,10 @@ const ButtonTest = () => (
<Button
href="/open-collective"
ref={(elem) => {
elem; // $ExpectType HTMLAnchorElement | null
expectType<HTMLAnchorElement | null, typeof elem>(elem);
}}
onClick={(e) => {
e; // $ExpectType MouseEvent<HTMLAnchorElement, MouseEvent>
expectType<React.MouseEvent<HTMLAnchorElement, MouseEvent>, typeof e>(e);
log(e);
}}
>
Expand All @@ -63,10 +64,10 @@ const ButtonTest = () => (
<Button<'div'>
component="div"
ref={(elem) => {
elem; // $ExpectType HTMLDivElement | null
expectType<HTMLDivElement | null, typeof elem>(elem);
}}
onClick={(e) => {
e; // $ExpectType MouseEvent<HTMLDivElement, MouseEvent>
expectType<React.MouseEvent<HTMLDivElement, MouseEvent>, typeof e>(e);
log(e);
}}
>
Expand Down
4 changes: 3 additions & 1 deletion packages/material-ui/src/TextField/TextField.spec.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import TextField from '@material-ui/core/TextField';
import { expectType } from '@material-ui/types';

{
// https://github.com/mui-org/material-ui/issues/12999
Expand Down Expand Up @@ -28,7 +29,8 @@ import TextField from '@material-ui/core/TextField';
InputProps={{ classes: { inputAdornedStart: 'adorned-start' } }}
onChange={(event) => {
// type inference for event still works?
const value = event.target.value; // $ExpectType string
const value = event.target.value;
expectType<string, typeof value>(value);
}}
/>
);
Expand Down

0 comments on commit 9bd4277

Please sign in to comment.