'Make a single property optional in TypeScript
In TypeScript, 2.2...
Let's say I have a Person type:
interface Person {
name: string;
hometown: string;
nickname: string;
}
And I'd like to create a function that returns a Person, but doesn't require a nickname:
function makePerson(input: ???): Person {
return {...input, nickname: input.nickname || input.name};
}
What should be the type of input
? I'm looking for a dynamic way to specify a type that is identical to Person
except that nickname
is optional (nickname?: string | undefined
). The closest thing I've figured out so far is this:
type MakePersonInput = Partial<Person> & {
name: string;
hometown: string;
}
but that's not quite what I'm looking for, since I have to specify all the types that are required instead of the ones that are optional.
Solution 1:[1]
You can also do something like this, partial only some of the keys.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface Person {
name: string;
hometown: string;
nickname: string;
}
type MakePersonInput = PartialBy<Person, 'nickname'>
Solution 2:[2]
Here is my Typescript 3.5+ Optional utility type
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
// and your use case
type MakePersonInput = Optional<Person, 'nickname'>
// and if you wanted to make the hometown optional as well
type MakePersonInput = Optional<Person, 'hometown' | 'nickname'>
Solution 3:[3]
For a plug and play solution, consider using the brilliant utility-types
package:
npm i utility-types --save
Then simply make use of Optional<T, K>
:
import { Optional } from 'utility-types';
type Person = {
name: string;
hometown: string;
nickname: string;
}
type PersonWithOptionalNickname = Optional<Person, 'nickname'>;
// Expect:
//
// type PersonWithOptionalNickname {
// name: string;
// hometown: string;
// nickname?: string;
// }
Solution 4:[4]
Update:
As of TypeScript 2.8, this is supported much more concisely by Conditional Types! So far, this also seems to be more reliable than previous implementations.
type Overwrite<T1, T2> = {
[P in Exclude<keyof T1, keyof T2>]: T1[P]
} & T2;
interface Person {
name: string;
hometown: string;
nickname: string;
}
type MakePersonInput = Overwrite<Person, {
nickname?: string;
}>
function makePerson(input: MakePersonInput): Person {
return {...input, nickname: input.nickname || input.name};
}
As before, MakePersonInput
is equivalent to:
type MakePersonInput = {
name: string;
hometown: string;
} & {
nickname?: string;
}
Outdated:
As of TypeScript 2.4.1, it looks like there's another option available, as proposed by GitHub user ahejlsberg in a thread on type subtraction: https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-307871458
type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Overwrite<T, U> = { [P in Diff<keyof T, keyof U>]: T[P] } & U;
interface Person {
name: string;
hometown: string;
nickname: string;
}
type MakePersonInput = Overwrite<Person, {
nickname?: string
}>
function makePerson(input: MakePersonInput): Person {
return {...input, nickname: input.nickname || input.name};
}
According to Intellisense, MakePersonInput
is equivalent to:
type MakePersonInput = {
name: string;
hometown: string;
} & {
nickname?: string;
}
which looks a little funny but absolutely gets the job done.
On the downside, I'm gonna need to stare at that Diff
type for a while before I start to understand how it works.
Solution 5:[5]
The type-fest package has a utility SetOptional
- https://github.com/sindresorhus/type-fest/blob/main/source/set-optional.d.ts
import { SetOptional } from 'type-fest';
type PersonWithNicknameOptional = SetOptional<Person, 'nickname'>
I find the library is well-maintained, and supports latest versions of typescript. It's worth adding in a typescript project IMO.
Solution 6:[6]
Ok well what you are really describing is two different "Types" of people (i.e. Person types) .. A normal person and a nick named person.
interface Person {
name: string;
hometown: string;
}
interface NicknamedPerson extends Person {
nickname: string;
}
Then in the case where you don't really want a nicknamed person but just a person you just implement the Person interface.
An alternative way to do this if you wanted to hang on to just one Person interface is having a different implementation for a non nicknamed person:
interface Person {
name: string;
hometown: string;
nickname: string;
}
class NicknamedPerson implements Person {
constructor(public name: string, public hometown: string, public nickname: string) {}
}
class RegularPerson implements Person {
nickname: string;
constructor(public name: string, public hometown: string) {
this.nickname = name;
}
}
makePerson(input): Person {
if(input.nickname != null) {
return new NicknamedPerson(input.name, input.hometown, input.nickname);
} else {
return new RegularPerson(input.name, input.hometown);
}
}
This enables you to still assign a nickname (which is just the persons name in case of an absence of a nickname) and still uphold the Person interface's contract. It really has more to do with how you intend on using the interface. Does the code care about the person having a nickname? If not, then the first proposal is probably better.
Solution 7:[7]
After a lot of digging, I think what I'm trying to do just isn't possible in TypeScript... yet. When spread/rest types land, I think it will be, though, with syntax something along the lines of { ...Person, nickname?: string }
.
For now, I've gone with a more verbose approach, declaring the properties that are required:
type MakePersonInput = Partial<Person> & {
name: string;
hometown: string;
};
function makePerson(input: MakePersonInput): Person {
return {...input, nickname: input.nickname || input.name};
}
This unfortunately requires me to update MakePersonInput
whenever I add more required properties to Person
, but it's impossible to forget to do this, because it will cause a type error in makePerson
.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Damian Pieczy?ski |
Solution 2 | |
Solution 3 | Philip Bulley |
Solution 4 | |
Solution 5 | Peter Krieg |
Solution 6 | |
Solution 7 | DallonF |