TypeScript: function that returns a generic Type which extends Record<string, string>

Viewed 2507

I want to create a parse<Type>() function that parses a string and returns an object, but I want parse() to return the correct type, and I am struggling.

type Identity = {
  name: string;
};

type ContactDetails = {
  phone: string;
  email: string;
};

function parse<T extends Record<string, string>>(input: string): T {
  const result = {};
  input.split('&').forEach(bits => {
    const [name, value] = bits.split('=');
    result[name] = value;
  });
  return result;
}

parse<Identity>('name=Paulin');
parse<ContactDetails>('phone=0123456789&email=paulin@email.com');

I get a TypeScript error:

Type '{}' is not assignable to type 'T'. '{}' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Record'.ts(2322)

I understand what the problem is: {} is not compatible with a generic Record type. But how can I solve this?

Thank you very much!

2 Answers

I'm fairly sure this is one of the places you have to resort to a type assertion. In this case:

function parse<T extends Record<string, string>>(input: string): T {
  const result: Record<string, string> = {};
// −−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^
  input.split('&').forEach(bits => {
    const [name, value] = bits.split('=');
    result[name] = value;
  });
  return result as T;
// −−−−−−−−−−−−^^^^^
}

On the playground

It's important to understand the limitations here, though. If your Identity or ContactDetails types had specific implementations (they don't in your example, they're just interfaces), the object parse returns won't be backed by that implementation. It is just a Record<string, string>. So you might be better off having parse return a Record<string, string> instead, and leave it to the caller to make an informed decision about the type.

Based on what T.J. Crowder suggested: leave it to the caller to make the decision about the type. here is my final implementation:

type Identity = {
  name: string;
};

type ContactDetails = {
  phone: string;
  email: string;
};

function parse(input: string): Record<string, string> {
  const result: Record<string, string> = {};
  input.split('&').forEach(bits => {
    const [name, value] = bits.split('=');
    result[name] = value;
  });
  return result;
}

const identity = parse('name=Paulin') as Identity;
const details = parse('phone=0123456789&email=paulin@email.com') as ContactDetails;

Related