Convert a type name to the actual type

1.5k Views Asked by At

Suppose we have the following enum

enum PrimayValType {
  "string",
  "boolean",
  "integer",
  "float"
}

Now i want to write a function that inputs a Buffer and a parameter of type PrimaryValType and converts the buffer based on the PrimaryValType. How to write such a function in Typescript?

function f(b: Buffer, t: PrimayValType): ??? {
    // converts b to a literal based on t
}

const b = Buffer.from('test', 'utf-8');
f(b, PrimayValType.string) // should be of type string
2

There are 2 best solutions below

0
kaya3 On BEST ANSWER

You can write the return type of your function as a mapped type. First, define PrimaryValType as a mapping from the string literals to the actual types:

type PrimaryValType = {
    "string": string,
    "boolean": boolean,
    "integer": number,
    "float": number,
}

Then given a string of type K extends keyof PrimaryValType, we can map it to the correct return type using the mapped type PrimaryValType[K].

To parse the input as the right type, you can switch on the type-string:

function parse<K extends keyof PrimaryValType>(s: string, t: K): PrimaryValType[K] {
    switch (t) {
        case "integer": return parseInt(s) as PrimaryValType[K];
        case "float":   return parseFloat(s) as PrimaryValType[K];
        case "boolean": return (s === "true") as PrimaryValType[K];
        case "string":  s as PrimaryValType[K];

        default: throw new Error("Illegal t: " + t);
    }
}

The type assertions are needed because Typescript can't tell that when t === 'integer' then K can't be 'string', for example. The code can be neatened by storing the parser functions in an object, as in @kmos.w's answer:

const parsers: { [K in keyof PrimaryValType]: (s: string) => PrimaryValType[K] } = {
    "integer": parseInt,
    "float":   parseFloat,
    "boolean": s => s === "true",
    "string":  s => s,
};

function parse<K extends keyof PrimaryValType>(s: string, t: K): PrimaryValType[K] {
    return parsers[t](s) as PrimaryValType[K];
}

The type assertion is still needed, for the same reason.

Playground Link

0
kmos.w On

[1]. Advice: Enums in typescript are used to define named constants. So it is better to qualify the string literals by names e.g.

enum PrimayValType {
  STRING = 'string',
  BOOLEAN = 'boolean',
  INTEGER = 'integer',
  FLOAT   = 'float',
}

[2]. Try this:

function f(b: Buffer, t: PrimayValType=PrimayValType.STRING): number | string | boolean {
  const value = b.toString('utf-8');
  switch (t) {
    case PrimayValType.BOOLEAN:
      const isBool = /true|false/.test(value);
      if (!isBool) {
        throw new TypeError(`${value} is invalid for a boolean type`);
      }
      return /true/.test(value);
    case PrimayValType.INTEGER:
    case PrimayValType.FLOAT:
      const isNaN = Number.isNaN(value);
      if (isNaN) {
        throw new TypeError(`${value} is invalid for a numeric type`);
      }
      return t === PrimayValType.INTEGER ? Number.parseInt(value) : Number.parseFloat(value);
    default:
      return value;
  }
}

essertEqual(f(Buffer.from("3.14", "utf-8"), PrimayValType.FLOAT), 3.14);
assertEqual(f(Buffer.from("3.14", "utf-8"), PrimayValType.INTEGER), 3);
assertTrue(f(Buffer.from("true", "utf-8")));

Revised answer. The Typescript way. (I haven't tested this):


const parseBool = (value: string) => {
    if (!/true|false/.test(value)) throw new TypeError(`${value} is invalid for a boolean type`);
    return /true/.test(value);
};


type PrimType = "string" | "boolean" | "integer" | "float";
type Convert<T> = (str: string) => T;
type Convertors<T> = { [t in PrimType]: Convert<T> };

const convertors: Convertors<number|string|boolean> = {
    "integer": parseInt,
    "float": parseFloat,
    "boolean": parseBool,
    "string": (str) => str,
};

const f = (b: Buffer, t: PrimType) => convertors[t](b.toString('utf-8'));