Make one 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 would like to create a function that returns Person, but does not require a nickname:
function makePerson(input: ???): Person {
return {...input, nickname: input.nickname || input.name};
}
What should be the type input
? I am looking for a dynamic way to specify a type that is identical Person
, except that nickname
is optional ( nickname?: string | undefined
). The closest I've figured out so far is the following:
type MakePersonInput = Partial<Person> & {
name: string;
hometown: string;
}
but this is not exactly what I'm looking for as I have to specify all types that are required instead of those that are optional.
source to share
Update:
As of TypeScript 2.8, this is supported much more succinctly by conditional types! So far, this also seems to be more robust 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
equivalent to:
type MakePersonInput = {
name: string;
hometown: string;
} & {
nickname?: string;
}
Deprecated:
As of TypeScript 2.4.1, it looks like there is another option suggested by GitHub user ahejlsberg on a subtraction thread: 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
equivalent to:
type MakePersonInput = {
name: string;
hometown: string;
} & {
nickname?: string;
}
which looks a little funny but absolutely gets the job done.
On the other hand, I will need to look at this type Diff
for a while before I begin to understand how it works.
source to share
Well, what you are really describing are two different "types" of people (ie types of people). An ordinary person and a nickname named by a person.
interface Person {
name: string;
hometown: string;
}
interface NicknamedPerson extends Person {
nickname: string;
}
Then in case 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 want to connect to one Person interface, has a different implementation for the person with no name:
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 allows you to still assign an alias (which is only the person's name in the absence of an alias) and still maintain a contract with the Person interface. It really has more to do with how you are going to use the interface. Does the code mean that the person has a nickname? If not, then the first sentence is probably better.
source to share
After a lot of digging, I think that what I am trying to do is not yet possible in TypeScript .... When spread / rest types , I think it will be, with syntax, something like strings { ...Person, nickname?: string }
.
I am currently taking a more verbose approach by declaring the required properties:
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 additional properties to Person
, but this is impossible to forget because it will throw a type error in makePerson
.
source to share