1

In my code below I am declaring all allowed strings twice, once in the type declaration and once in the type guard. How can I rewrite my code to avoid this duplication?

// inspired by: https://github.com/mattdesl/parse-unit

export type ParsedValue = {
  value: number;
  unit: Unit;
};

type Unit =
  | "ch"
  | "ex"
  | "em"
  | "rem"
  | "in"
  | "cm"
  | "mm"
  | "pt"
  | "pc"
  | "px";

export function parseUnit(str: string | null): ParsedValue | null {
  if (!str) {
    return null;
  }

  var value = parseFloat(str);

  const match = str.match(/[\d.\-\+]*\s*(.*)/);
  const unit = match ? match[1] : "";
  if (!isUnit(unit)) {
    return null;
  }

  return { value, unit };
}

export function isUnit(str: string): str is Unit {
  return ["ch", "ex", "em", "rem", "in", "cm", "mm", "pt", "pc", "px"].includes(
    str
  );
}

EDIT:

Here is the current version after some suggestions. This verison doesn't work. On return { value, unit } I get Type 'string' is not assignable to type '"ch" | "ex" | "em" | "rem" | "in" | "cm" | "mm" | "pt" | "pc" | "px"'.

// inspired by: https://github.com/mattdesl/parse-unit

export type ParsedValue = {
  value: number;
  unit: Unit;
};

const units = [
  "ch",
  "ex",
  "em",
  "rem",
  "in",
  "cm",
  "mm",
  "pt",
  "pc",
  "px",
] as const;

type Unit = typeof units[number];

export function parseUnit(str: string): ParsedValue | null {
  if (!str) {
    return null;
  }

  var value = parseFloat(str);

  const match = str.match(/[\d.\-\+]*\s*(.*)/);
  const unit = match ? match[1] : "";
  if (!isUnit(str)) {
    return null;
  }

  return { value, unit };
}

export function isUnit(str: string): str is Unit {
  return ((units as unknown) as Array<string>).includes(str);
}
user1283776
  • 19,640
  • 49
  • 136
  • 276
  • You can extract the answer from responses of SO question: https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values – Karthick Vinod May 14 '20 at 08:00
  • 1
    Does this answer your question? [Typescript derive union type from tuple/array values](https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values) – Aleksey L. May 14 '20 at 08:01
  • @AlekseyL: Not fully, I still get an error when I try to check if the str is included in the array at runtime, and then try to return a typed value, see edit – user1283776 May 14 '20 at 09:00
  • probably the easiest would be `isUnit(str: any)..` – Aleksey L. May 14 '20 at 09:43

1 Answers1

0

Declare the array as a standalone variable, as const so its type doesn't get widened to string[], then define the Unit type as typeof units[number]:

const units = ["ch", "ex", "em", "rem", "in", "cm", "mm", "pt", "pc", "px"] as const;
type Unit = typeof units[number];
export function isUnit(str: string): str is Unit {
  return (units as unknown as Array<string>).includes(str);
}

The units is a ReadOnlyArray which doesn't accept arbitrary strings to be passed to .includes, unfortunately, so it has to be turned into Array<string> by going through unknown first.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I get an error on 'return units.includes(str)': Argument of type 'string' is not assignable to parameter of type '"ch" | "ex" | "em" | "rem" | "in" | "cm" | "mm" | "pt" | "pc" | "px"'. – user1283776 May 14 '20 at 08:20
  • Oh, I see that too. That's unfortunate, it looks like you have to put it through `unknown` first so `.includes` can accept the string – CertainPerformance May 14 '20 at 08:27
  • Hmm, I am still not able to get this working. See the code I added in my edit – user1283776 May 14 '20 at 09:02
  • You're testing the wrong thing: `if (!isUnit(str)) {` but `str` is the input string before the regex has parsed it. Pretty sure you meant `if (!isUnit(unit)) {` – CertainPerformance May 14 '20 at 09:14