Can I incrementally build an object that implements an interface?

Viewed 94

I have an Interface like:

export interface Person {
  name: string;
  isMillenial: boolean;
}

And a function that outputs that interface like:

function makePerson(name: string, birthYear: number): Person {
  let newPerson = {
    name
  }

  // Calculate various properties, slowly building up the `newPerson` object
  newPerson.isMillenial = birthYear < 1981

  return newPerson
}

However this will fail with an error like:

Property 'isMillenial' is missing in type '{ name: string }' but required in type 'Person '.

I know I could declare all the properties of newPerson when I create it, to match what's expected - but I'd like to build the object I'm creating incrementally, adding properties until the object is complete. Is there a way I can build the object incrementally in TypeScript while still satisfying the type checking?

(If the answer is 'no, you need defaults', that's an acceptable answer)

1 Answers

Yes, but you'll have to declare it differently, and you may have to tell TypeScript when you've completed the object. The Partial utility type creates a type based on another type that makes all of its properties optional. So:

function makePerson(name: string, birthYear: number): Person {
    let newPerson: Partial<Person> = {
    // −−−−−−−−−−−−^^^^^^^^^^^^^^^
        name
    };

    // Calculate various properties, slowly building up the `newPerson` object
    newPerson.isMillenial = birthYear < 1981;

    return newPerson as Person;
    // −−−−−−−−−−−−−−^^^^^^^^^
}

Sadly the type assertion at the end is necessary (which surprised me, you can tell from the above that neither property will be undefined by the end, but the TypeScript team have to put a limit on how smart the compiler is to keep it from being really slow).

Playground link

The as Person on the return also means you can leave it off the function declaration signature if you want, TypeScript will know the return type of the function:

function makePerson(name: string, birthYear: number): Person {
// Optional as far as TypeScript is concerned −−−−−−^^^^^^^^

...though you may want to leave it there if the function is more than a few lines, to ensure that you don't add a new return elsewhere that changes the return type.


You mentioned it in the question, but I'll emphasize it just for what it's worth (and for lurkers) that you can build up the information you're going to use to create the object, then create the object all at once to save needing a type annotation. That simplifies the example a fair bit, though of course real code may not get simpler:

function makePerson(name: string, birthYear: number): Person {
    const isMillenial = birthYear < 1981;
    return {name, isMillenial};
}

That's not always feasible, but when it is I find it useful.

Related