Skip to content

Latest commit

 

History

History
391 lines (288 loc) · 9.87 KB

typescript.md

File metadata and controls

391 lines (288 loc) · 9.87 KB

Typescript

In React

Polymorphic components

render as prop - use React.ElementType

interface IExampleProps {
  as: React.ElementType;
}

const Example = ({ as: Element = "button" }) => {
  return <Element></Element>;
};

Dynamic as prop

/**
 *  Type for `as` prop that accepts any HTML element as string or any React Function Component
 *
 *  Accepts optional interface for specifying which props the Function Component must accept
 */
export type AnyTag<Interface = any> =
  | keyof JSX.IntrinsicElements
  | React.FunctionComponent<Interface>
  | React.ForwardRefExoticComponent<Interface>
  | (new (props: Interface) => React.Component);

/**
 * Type for component that accepts dynamic `as` prop to unpack the props accepted by the injected component
 */
export type PropsOf<Tag> = Tag extends keyof JSX.IntrinsicElements
  ? JSX.IntrinsicElements[Tag]
  : Tag extends React.ComponentType<infer Props>
  ? Props & JSX.IntrinsicAttributes
  : Tag extends React.ForwardRefExoticComponent<infer Props>
  ? Props & JSX.IntrinsicAttributes
  : never;

/**
 * Type used in components with forwardRef and `as` dynamic prop to use the HTML element of `as` prop
 * @todo add support for extracting HTML element of React component that use forwardRef as well
 */
export type ElementOf<Tag> = Tag extends keyof HTMLElementTagNameMap
  ? HTMLElementTagNameMap[Tag]
  : Tag extends React.ForwardRefExoticComponent<any>
  ? HTMLElement
  : never;

/**
 * Type for component props that accept it's props, spacing props, ref and `as` prop to inject other component in render
 *
 * **NOTE**: The props accepted by the dynamic `as` component can override internal props of UI component
 * @see {@link https://stackoverflow.com/questions/54049871/how-do-i-type-this-as-jsx-attribute-in-typescript}
 */
export type UIComponentInjectableProps<
  Props,
  Tag extends AnyTag
> = Readonly<Props> & SpacingPropsType & PropsOf<Tag>;
export type UIComponentInjectable<Props, Tag extends AnyTag> = React.FC<
  UIComponentInjectableProps<Props, Tag>
>;

Private children

Component that can be used only in specific parent component as a child. For example: <List> (can have) => <ListItem>

  1. Make a component that doesn't render anything and throws an error instead. Set a property on the function ListItem.parent = AllowedParentComponent so that we can check it later.
  2. Make another component Private* that has the same props type and renders the content
  3. Inside the parent component when iterating over children (by using toArray) check for child.type and child.type.parent
type ListItemProps = {
  children: ReactText;
} & JSX.IntrinsicElements["li"];

type ChildWithParent = ReactElement & {
  type: { parent: ReactElement };
  props: ListItemProps;
};

const ListItem = (_props: ListItemProps) => {
  throw new Error("ListItem musi być dzieckiem List");
};

ListItem.parent = List;

export const List = ({ children }: ListProps) => {
  const newChildren = flattenChildren(children).map((child: unknown) => {
    const child = child as ChildWithParent;
    if (isValidElement(child)) {
      if (child.type === ListItem && child.type.parent === List) {
        return (
          <PrivateListItem
            key={child.key}
            {...(child.props as ListItemProps)}
          />
        );
      } else {
        throw new Error("Tylko ListItem może być dzieckiem List");
      }
    }
  });

  return <ul>{newChildren}</ul>;
};

const ListItemWrapper = (props: ListItemProps) => <ListItem {...props} />;

ListItemWrapper.parent = List;

Note: flattenChildren is provided by react-keyed-flatten-children Code from this blog

with eslint

setup

install plugin @typescript-eslint

npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser

set parser to @typescript-eslint/parser

notes

some eslint rules can be problematic:

  • no-undef - disable altogether as TypeScript has its own chek for that
  • no-unused-vars - use @typescript-eslint/no-unused-vars instead
  • no-use-before-define - use @typescript-eslint/no-use-before-define instead

enums

zbiór stałych wartości, domyślnie numerowany od 0

żeby używać jako typ union wszystkich wartości:

interface SomeInterface {
  type: keyof typeof MyTypes;
}

generics

Deep Dive Docs

Generics and arrow functions

Source

const foo = <T extends unknown>(x: T) => x;
// or
const foo = <T>(x: T) => x;

Preserve generic types in higher order functions (ex. forwardRef)

aka. forwardRef with generics

Source

const SelectWithRef = forwardRef(
  <Option extends string>(
    props: Props<Option>,
    ref?: Ref<HTMLSelectElement>
  ) => <Select<Option> {...props} forwardedRef={ref} />
);

More examples

Conditional types

Source

<A extends B>

means: A is a superset of B A has all of B properties (and maybe some more) A is possibly more specific version of B

A musi zawierać te same propertisy co B żeby przeszedł typecheck.

A extends { meow(): void } ? A : never

przejdzie tylko jak object który przekażemy będzie miał funkcję meow, z pasującym typem - jak nie to never, czyli nie może przejść

można pomyśleś że conditional type działa jak funkcja JSowa:

type isNumber<T> = T extends number ? "number" : "other";
const isNumber = (x) => {
  typeof x === "number" ? "number" : "other";
};

Source

infer

infer pozwala na użycie generycznego typu w conditional types i wyciągnięcie go

type PropsOf<T> = T extends React.ComponentType<infer Props> ? Props : never;

infer używa nazwy podanego typu np:

type Unpack<A> = A extends Array<infer E> ? E : A;

type Test = Unpack<Apple[]>;
// => Apple
type Test = Unpack<Apple>;
// => Apple

tutaj zwróciło Apple nawet jak podaliśmy array Apple

Source

Top and bottom types

  • Using any is like saying "I have no idea what this value looks like. So, TypeScript, please assume I'm using it correctly, and don't complain if anything I do seems dangerous".
  • Using unknown is like saying "I have no idea what this value looks like. So, TypeScript, please make sure I check what it is capable of at run time."
  • never is the bottom type (it literally means it can never happen)

Mapped type

Docs link

Creates new interface from union of keys or another interface with keyof (keyof turns interafece into union)

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Predefined types

Docs link

Typescript posiada gotowe mapped types do częstych przypadków

  • Partial<Type> - wszystkie parametry stają się opcjonalne
  • Required<Type> - odwrotnie do Partial, wszystko jest wymagane
  • Readonly<Type> - dodaje do każdego parametru readonly
  • Record<Keys, Type> - tworzy typ obiektu mappując keys na możliwe wartości podane w `Types
interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const nav: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" },
};
  • Pick<Type, Keys> - wybiera wybrane keys z podanego typu
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};
  • Omit<Type, Keys> - działa odwrotnie do Pick, wybiera wszystkie keys z typu poza podanymi

Funfact: Typescript pod spodem używa tak naprawdę conditional types + infer np.

// Obtain the parameters of a function type in a tuple
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

Adding members to a type

Use an intersection - &

type SomeType = {
  [P in keyof T]?: T[P];
} & { key: value };

Modifing global types

Source Sometimes TypeScript DOM types are broken (either spec is not up to date or other stuff)

To fix types:

  1. Add global declaration in your file or
  2. Add declaration for whole project, into folder that is defined in typeRoots
declare global {
  // opening up the namespace
  var ResizeObserver: {
    // mergin ResizeObserver to it
    prototype: ResizeObserver;
    new (callback: ResizeObserverCallback): ResizeObserver;
  };
}

if adding for whole project make shure that your type definitions file is included inside typeRoots:

{
  "compilerOptions": {
    //...
    "typeRoots": ["@types", "./node_modules/@types"]
    //...
  },
  "include": ["src", "@types"]
}

Tips and tricks

Type object keys dynamically

Create an object that will have predefined set of keys (for type checking) that can have one type (string) and specified value.

Example:

const filterFunctions = {
  date: (value, index) => true,
  string: (value, index) => false,
};

Solution:

type FilterFn = (data: SomeDataType, index: number) => boolean;

function filterFunctionsWrapper<T extends { [nkeyame: string]: FilterFn }>(
  functions: T
) {
  return functions;
}