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:
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" } });