Skip to content

Instantly share code, notes, and snippets.

@KATT
Last active July 26, 2023 06:16
Show Gist options
  • Star 61 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KATT/98f10e7d451b05c09378b2df589cd0fd to your computer and use it in GitHub Desktop.
Save KATT/98f10e7d451b05c09378b2df589cd0fd to your computer and use it in GitHub Desktop.
πŸ§™β€β™‚οΈ Favourite non-obvious TS tricks

Record<string, x | undefined>

Represent maps

// rather than 
const map: {[ key: string ]: string} = {
  foo: 'bar',
}

// do
const map: Record<string, string|undefined> = {
  foo: 'bar',
};

// ... or just do a Map
const map = new Map<string, string>()
const map = new Map([['foo', 'bar']])

Partial

<Formik
  initialValues={initialValues}
  enableReinitialize={true}
  validate={(values) => {
    const errors: Partial<typeof values> = {};
    if (
      values.contactEmail &&
      !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.contactEmail)
    ) {
      errors.contactEmail = 'Invalid email address';
    }
    return errors;
  }}
  // [..]
/>

Omit

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

ReturnType<typeof fn>

Typecast return value of function

ThenArg<TPromise>

Unwrap the value of a promise.

export type ThenArg<TPromise> = TPromise extends PromiseLike<infer TValue>
  ? ThenArg<TValue>
  : TPromise;

inferAsyncReturnType<typeof myFn>

export type inferAsyncReturnType<TFunction extends (...args: any) => any> =
  ThenArg<ReturnType<TFunction>>;

React.ComponentProps<typeof Component>

When making a wrapper of another react fn

export type MuiCreatableSelectProps = React.ComponentProps<
  typeof CreatableSelect
> & {
  label: string;
};

Combining with Omit if a value is not needed in wrapper

type OtherComponentProps = React.ComponentProps<
  typeof OtherComponent
>;
export type MyComponentProps = Omit<OtherComponentProps, 'value' | 'otherValueNotNeeded'>;

export function MyComponent(props: MyComponent) {
  // [..]
}

NonNullable<Something>

type Post = NonNullable<PostByIdQuery['Post_by_pk']>;

Avoiding export default

// Makes autocompletion works more reliably if you always do
export const MyComponent = () => { /* ... */ }

// instead of

export default () => { /* ... */ }

Options object over fn arguments

// bad
const fn = (arg1: string, arg2: string) => { /* ... */ }

// good
const fn = ({arg1, arg2}: {arg1: string, arg2: string}) => { /* ... */ }

Explicit state

type UseRemoteData =
  | {
      status: 'loading';
    }
  | {
      status: 'error';
      error: Error;
    }
  | {
      status: 'ok';
      result: RemoteDataResponse;
    };
function useRemoteData(token: string): UseRemoteData {
  const [result, setResult] = useState<UseRemoteData>({
    status: 'loading',
  });

  useEffect(() => {
    function load() {
      doAsyncStuff()
        .then((res) => {
          setResult({
            status: 'ok',
            result: res,
          });
        })
        .catch((error) => {
          setResult({
            status: 'error',
            error,
          });
        });
    }

    load();
  }, [token]);

  return result;
}
@aleksanderjessitm
Copy link

Great tips!

@bennidi
Copy link

bennidi commented Nov 24, 2022

I am currently enjoying to carry type information around using arrays with null values casted to the desired type.
They are basically only there to carry type information. I use these methods as sugar for something like [ null as unknown as DesiredType] as const
These are the utilitypes I use for this.

/** @summary Sugar to construct type information from null value */
export const Type = <T>():T => null as unknown as T

export function TypeTuple<T>(id:string): readonly [string, T ]{
  return [ id, Type<T>() ] as const
}
export function TypeTag<T>(): readonly [ T ]{
  return [ Type<T>() ] as const
}


export type InferElement<T> = T extends readonly [infer Element] ? Element : never;

export type MappedArrayTypeHolder<Arrays> = {
  [I in keyof Arrays]: InferElement<Arrays[I]>
};

Then I can define typetags like this const SessionManagerTag = TypeTag<SessionManager>() and use this as the key for deploying and resolving this object with correct typings like this registry.deploy( [ [ SessionManagerTag, new SessionManager(eventbus, stores) ] ] as const) and const [manager, session] = registry.discover([SessionManagerTag, UserSessionTag] as const)

The registry is very simple wrapper around a standard Map and is only there to resolve the typings correctly.

export class Registry {

  private registry = new Map()

  constructor(initial?:[string, any][]){
    initial?.forEach(([key, value]) => this.registry[key] = value)
  }

  discover = <T extends ReadonlyArray<readonly [any]>>(args: T): MappedArrayTypeHolder<T> => 
    // the type inference is not strong enough here, so the case is needed
    args.map((key) => this.registry.get(key)) as unknown as MappedArrayTypeHolder<T>


  deploy = <T extends ReadonlyArray<readonly [any, any]>>(args: T): MappedTuples<T>  => 
    args.map(([key, value]) => {
      this.registry.set(key, value)
      return value
    }) as unknown as MappedTuples<T>

 

}


// And somewhere else in the code

// ... returns the deployed values with correct typings
const [manager] = registry.deploy( [ [ SessionManager.TAG, new SessionManager(eventbus, stores) ] ] as const)


// Resolve multiple deployed values by their type tags 
const [config, session] = registry.discover([ConfigTag, UserSessionTag] as const)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment