Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A wrapper with Typescript #6

Closed
loia5tqd001 opened this issue Nov 17, 2021 · 3 comments
Closed

A wrapper with Typescript #6

loia5tqd001 opened this issue Nov 17, 2021 · 3 comments

Comments

@loia5tqd001
Copy link

Here is a Typescript wrapper around this package I write for my project (with a very good type-inference). The API has changed a little bit (initial and types are now combined into 1 param called initialOrType for more concise syntax).

Usage

const [params, setParams] = useUrlSearchParams({ name: 'loi', age: Number, handsome: true }); // see value ('loi') and type (Number) are mixed

Now, the initial URLSearchParams is ?name=loi&handsome=true, with the type of params to be

{ 
  name?: string, 
  age?: number, 
  handsome?: boolean 
}

Or, if you want to take full advantage of typescript to narrow the type:

type Params = { name:'loi' | 'Loi', age: number, handsome: true }
const [params, setParams] = useUrlSearchParams({ name: 'loi', age: 21, handsome: true } as Params);

Now the type of params becomes

{ 
  name?: 'loi' | 'Loi' /* instead of string */, 
  age?: number, 
  handsome?: true /* instead of boolean */ 
}

Or you can mix initial params with types (in case not all types have initial params) and still narrow the type:

const [params, setParams] = useUrlSearchParams({
  name: 'loi' as 'loi | 'Loi',
  age: Number,
  handsome: true as true
});

will result in the same type as the example above.

File

(I will update immediately here if there's any bug)

useUrlSearchParams.ts

import { useUrlSearchParams as useOriginal } from 'use-url-search-params';

/**
 * mix of Constructor and initial params
 * e.g: TProps { a: '1', b: Number } will result to:
 * - type typescript of { a: string, b: number }
 * - type check (second param of useOriginal) of { a: String, b: Number }
 * - initial params of { a: '1' } or ?a=1
 */
type TProps = Readonly<{
  [key: string]:
    | NumberConstructor
    | StringConstructor
    | BooleanConstructor
    | DateConstructor
    | number
    | string
    | boolean
    | Date;
  // See all the types supported by use-url-search-params here:
  // https://github.com/rudyhuynh/use-url-search-params/blob/95b0880bf9e2bf84e19c779b2a4078a47271ea9c/src/useUrlSearchParams.js#L4
}>;

/**
 * Convert from TProps to typescript type of the params
 * e.g: TProps { a: '1', b: Number } will result to:
 * { a: string, b: number }
 */
type GetUrlSearchParams<T extends TProps> = {
  // We have the ? (optional) here because URLSearchParams is an external thing, so we cannot control whether it is presented as we expected it to be
  [K in keyof T]?: T[K] extends NumberConstructor
    ? number
    : T[K] extends StringConstructor
    ? string
    : T[K] extends BooleanConstructor
    ? boolean
    : T[K] extends DateConstructor
    ? Date
    : T[K];
};

/**
 *
 * @param value mixed of initial param values and Constructors
 * @returns whether the "value" is just a value, not a Constructor
 * e.g: '1' => true, Number => false, String => false
 */
const isNotConstructor = (
  value: TProps[string],
): value is string | number | boolean | Date =>
  typeof value === 'string' ||
  typeof value === 'number' ||
  typeof value === 'boolean' ||
  value instanceof Date;

/**
 * wrapper around the original "useUrlSearchParams":
 * https://github.com/rudyhuynh/use-url-search-params
 */
export function useUrlSearchParams<Props extends TProps>(initialOrType: Props) {
  // exptract "initial" from "initialOrType"
  // desired result: { param1: 'value1', param2: 123456 }
  const initParams = Object.fromEntries(
    // Learn the Object.fromEntries/Object.entries combination here:
    // https://stackoverflow.com/a/14810722/9787887
    Object.entries(initialOrType).filter(([_, value]) =>
      isNotConstructor(value),
    ),
  );

  // exptract "type" from "initialOrType"
  // desired result: { param1: String, param2: Number }
  const paramsTypes = Object.fromEntries(
    Object.entries(initialOrType).map(([key, value]) => [
      key,
      isNotConstructor(value) ? value.constructor : value,
    ]),
  );

  const [params, setParams] = useOriginal(initParams, paramsTypes);

  // The setParams will add params to current params, NOT flush out all current params
  // E.g: ?a=1&b=2, setParams({a:3}) will result in ?a=3&b=2
  // look at its implementation here:
  // https://github.com/rudyhuynh/use-url-search-params/blob/95b0880bf9e2bf84e19c779b2a4078a47271ea9c/src/useUrlSearchParams.js#L11
  return [params, setParams] as unknown as [
    GetUrlSearchParams<Props>,
    (newParams: GetUrlSearchParams<Props>) => void,
  ];
}
@rudyhuynh
Copy link
Owner

Thank you very much for this, master branch is refactored all to typescript now and released at version 2.4.4.
Closing this now.

@loia5tqd001
Copy link
Author

loia5tqd001 commented Jan 15, 2022

I would say the typescript from this issue has better type-inference than your newly refactored TS code. Moreover, this issue's code has a slightly different (better, more concise) API than the package, I think it's worth leaving this issue opened until the original package fully covers or is fully better than the issue. wdyt?

About the release, I think you better off release minor version upgrade instead of patch version.

Thank you for the work btw, I really appreciate it.

@rudyhuynh
Copy link
Owner

rudyhuynh commented Jan 20, 2022

Your wrapper does not cover my case where types is Array of available string values (like enum) and A custom resolver function. That's why I can not use your wrapper.
I still think we should pass two params to the hook, one is initial value and the other one is type definition, it is easier to understand and more flexible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants