Make a single property optional in TypeScript

后端 未结 6 1721
盖世英雄少女心
盖世英雄少女心 2020-12-14 05:42

In TypeScript, 2.2...

Let\'s say I have a Person type:

interface Person {
  name: string;
  hometown: string;
  nickname: string;
}

相关标签:
6条回答
  • 2020-12-14 06:24

    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.

    0 讨论(0)
  • 2020-12-14 06:31

    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;
    // }
    
    0 讨论(0)
  • 2020-12-14 06:34

    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.

    0 讨论(0)
  • 2020-12-14 06:39

    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'>
    
    0 讨论(0)
  • 2020-12-14 06:39

    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.

    0 讨论(0)
  • 2020-12-14 06:44

    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'>
    
    0 讨论(0)
提交回复
热议问题