Reputation: 3286
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.
Upvotes: 215
Views: 166332
Reputation: 11
I reformatted your code to this:
interface Person {
name: string;
hometown: string;
nickname?: string;
}
TypeScript 5. The question mark "?" denotes an optional property or method in an interface or class.
In the code I provided, the "nickname" property is marked with a question mark, which means that it's optional. It indicates that instances of the Person interface may or may not have a nickname property.
If a Person object has a nickname property, it must be a string type. However, if it doesn't have a nickname property, it won't cause a compilation error. This makes it possible to define objects that conform to the Person interface with or without a nickname property.
Upvotes: -3
Reputation: 6703
export type Optional<T, K extends keyof T> = Omit<T, K> & { [P in keyof T]?: T[P] | undefined; }
If you want to cover the case when the key is not passed at all, or it is passed but with value set to undefined
Upvotes: 0
Reputation: 1181
If you use a recent version of typescript, a simple solution is to do
function makePerson(input: Omit<Person, 'nickname'> & { nickname?: string }): Person {
return {...input, nickname: input.nickname || input.name};
}
Basically you remove the "nickname" property from the interface and re-add it as optional
If you want to make sure to keep it in sync with the original interface you can do
Omit<Person, 'nickname'> & Partial<Pick<Person, 'nickname'>>
which will warn you if you ever change the "nickname" prop in the original interface
Upvotes: 13
Reputation: 422
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.
Upvotes: 17
Reputation: 3819
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'>
Upvotes: 227
Reputation: 9414
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;
// }
Upvotes: 48
Reputation: 3326
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'>
Upvotes: 318
Reputation: 3286
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.
Upvotes: 22
Reputation: 3286
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
.
Upvotes: 0
Reputation: 824
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.
Upvotes: 1