Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Make properties optional

In a TypeScript project, there’s a type definition like this:

type Foo = {
  a: string;
  b: string;
  metadata: {
    c: string;
    d: string;
  };
};

It’s imported from elsewhere and used a lot elsewhere, so I can’t change it.

I’d like to create a function that takes a Foo as its parameter, but with a and metadata.c being optional because I can set meaningful default values for those fields in this context:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

function doSomething(foo: {
  a?: string;
  b: string;
  metadata: {
    c?: string;
    d: string;
  };
}): void {
  // do whatever
}

The actual type is a lot bigger though and gets updated from time to time, so completely copying its definition to add two question marks is not an option.

Here’s what I’ve tried:

Foo & { a?: string; metadata: { c?: string } }

Doesn’t work because this creates a type that is a Foo and a { a?: string; metadata: { c?: string } } at the same time. This is basically just Foo because the required properties of Foo "win". Would probably work if I had the inverse problem (optional properties in Foo that are required here).

Exclude<Foo, { a?: string; metadata: { c?: string } }>

Doesn’t work because Foo is not a type union, and Exclude can only exclude types from type unions. It can’t "subtract" individual properties from a type defined by a type literal.

Partial<Foo>

Makes all properties of Foo optional. I don’t want that.

Omit<Foo, 'a'> & { a?: string }

Omit successfully removes a from Foo. I can then add a back in as an optional property. However, I don’t know what to do about c because it is nested inside metadata.

What’s the best way to go about this?

>Solution :

You can derive a type from Foo for the doSomething parameter, without modifying Foo. Because TypeScript’s type system is structural (based on the shapes of types), not nominal (based on the names/identities of shapes), you can keep Foo assignment-compatible to your new type, meaning you can pass Foo-typed arguments to doSomething, but you can also pass arguments with your new type.

Here’s one way to define the new type — note that we get all the type information from Foo other than what we need to adjust a and metadata.c:

type SpecialFoo = Omit<Foo, "a" | "metadata"> & {
    a?: Foo["a"];
    metadata: Omit<Foo["metadata"], "c"> & {
        c?: Foo["metadata"]["c"];
    };
};

Then doSomething accepts one of those:

function doSomething(foo: SpecialFoo): void {
    // do whatever
    console.log(foo);
}

With that, these work:

// Works
doSomething({ b: "b", metadata: { d: "d" } });
// Works
doSomething({ a: "a", b: "b", metadata: { c: "c", d: "d" } });

But as one would want, these examples with invalid values for a and c don’t work:

// Error as desired, `a` has the wrong type
doSomething({ a: 42, b: "b", metadata: { d: "d" } });
// Error as desired, `c` has the wrong type
doSomething({ b: "b", metadata: { c: 42, d: "d" } });

Playground link

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading