DallonF
DallonF

Reputation: 3286

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.

Upvotes: 215

Views: 166332

Answers (10)

mercy ikpe
mercy ikpe

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

Lachezar
Lachezar

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

Alex Beauchemin
Alex Beauchemin

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

Peter Krieg
Peter Krieg

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

Tim Krins
Tim Krins

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

Philip Bulley
Philip Bulley

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

Damian Pieczyński
Damian Pieczyński

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

DallonF
DallonF

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

DallonF
DallonF

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

Brian Gorman
Brian Gorman

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

Related Questions