【问题标题】:Typescript interface default valuesTypescript 接口默认值
【发布时间】:2016-05-06 14:07:49
【问题描述】:

我在 TypeScript 中有如下界面:

interface IX {
    a: string,
    b: any,
    c: AnotherType
}

我声明一个该类型的变量并初始化所有属性

let x: IX = {
    a: 'abc',
    b: null,
    c: null
}

然后我稍后在 init 函数中为它们分配实际值

x.a = 'xyz'
x.b = 123
x.c = new AnotherType()

但我不喜欢在声明对象时为每个属性指定一堆默认的空值,因为它们稍后将被设置为实际值。我可以告诉界面将我不提供的属性默认为 null 吗?什么会让我这样做:

let x: IX = {
    a: 'abc'
}

不会出现编译器错误。现在它告诉我

TS2322:类型“{}”不可分配给类型 '九'。类型“{}”中缺少属性“b”。

【问题讨论】:

标签: typescript


【解决方案1】:

另一种方法是使用https://www.npmjs.com/package/merge
这与上一个答案相同,但更整洁。

让我们安装合并

yarn add -D merge

接下来让我们创建一个带有一些选项的界面。
我们会将其放入
./types/index.ts

export interface ExampleOpts {
    opt1: string,
    opt2: string,
    opt3: string,
}

接下来让我们创建一组默认值
您可以将其放在同一个文件中,但让类型分开并将其放入
./config/index.ts

import { ExampleOpts } from '../types'

// Defaults
export const ExampleOptsDefault : ExampleOpts = {
    opt1: 'default value 1',
    opt2: 'default value 2',
    opt3: 'default value 3',
}

接下来让我们用
./index.ts

中的函数将所有内容连接在一起
import { ExampleOpts } from './types'
import { ExampleOptsDefault } from './config'
import merge from 'merge'

// The ? makes the parameter optional
export function test1(options?: ExampleOpts) {
    // merge tries to load in the defaults first, then options next if it's defined
    const merged_opts: ExampleOpts = merge.recursive(ExampleOptsDefault, options)
    // log the result to the console
    console.log(merged_opts)
}

【讨论】:

    【解决方案2】:

    如果您有很多参数,最好让用户只插入少量参数而不是按特定顺序插入。

    例如,不好的做法:

    foo(a?, b=1, c=99, d=88, e?)
    foo(null, null, null, 3)
    

    因为您必须在实际需要的参数之前提供所有参数 (d)。

    使用的好习惯是:

    foo({d=3})
    

    做到这一点的方法是通过接口。 您需要将参数定义为如下接口:

    interface Arguments {
        a?;
        b?; 
        c?;
        d?;
        e?;
    }
    

    并定义如下函数:

    foo(arguments: Arguments)
    

    现在接口变量不能获取默认值,那么我们如何定义默认值呢?

    简单,我们为整个界面定义默认值:

    foo({
            a,
            b=1,
            c=99,
            d=88,
            e                    
        }: Arguments)
    

    现在如果用户通过:

    foo({d=3})
    

    实际参数为:

    {
        a,
        b=1,
        c=99,
        d=3,
        e                    
    }
    

    另一个不声明接口的选项是:

    foo({
            a=undefined,
            b=1,
            c=99,
            d=88,
            e=undefined                    
        })
    

    跟进: 在前面的函数定义中,我们为参数对象的fields定义了默认值,但没有为对象本身定义默认值。 因此,我们将从以下调用中得到一个提取错误(例如Cannot read property 'b' of undefined):

    foo()
    

    有两种可能的解决方案:

    1. const defaultObject = {a=undefined, b=1, c=99, d=88, e=undefined}
      function foo({a=defaultObject.a, b=defaultObject.b, c=defaultObject.c, d=defaultObject.d, e=defaultObject.e} = defaultObject)
      
    2. const defaultObject = {a=undefined, b=1, c=99, d=88, e=undefined}
      function foo(object) {
          const {a,b,c,d,e} = {
              ...defaultObject,
              ...object,
          }
          //Continue the function code..
      }
      

    【讨论】:

      【解决方案3】:

      您还可以有一个帮助方法/函数,它返回具有默认属性值的对象,然后调用代码可以根据需要覆盖默认值。这就是我正在遵循的方法,因为我在当前项目中遇到了同样的问题。这种编码默认属性值对象的方式是一次性的,您可以在整个应用程序中重用该对象。

      【讨论】:

        【解决方案4】:

        我使用以下模式:

        创建实用程序类型Defaults<T>

        type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
        type Defaults<T> = Required<Pick<T, OptionalKeys<T>>>
        

        使用选项/默认值声明类:

        // options passed to class constructor
        export interface Options {
            a: string,
            b?: any,
            c?: number
        }
        
        // defaults
        const defaults: Defaults<Options> = {
            b: null,
            c: 1
        };
        
        export class MyClass {
            // all options in class must have values
            options: Required<Options>;
        
            constructor(options: Options) {
                // merge passed options and defaults
                this.options = Object.assign({}, defaults, options);
            }
        }
        

        创建类实例:

        const myClass = new MyClass({
            a: 'hello',
            b: true,
        });
        
        console.log(myClass.options);
        // { a: 'hello', b: true, c: 1 }
        

        【讨论】:

          【解决方案5】:

          这取决于情况和用法。一般来说,TypeScript 中的接口没有默认值。

          如果您不使用默认值
          您可以将x 声明为:

          let x: IX | undefined; // declaration: x = undefined
          

          然后,在您的 init 函数中,您可以设置实际值:

          x = {
              a: 'xyz'
              b: 123
              c: new AnotherType()
          };
          

          这样,x 可以是未定义的或已定义的——undefined 表示对象未初始化,如果不需要,则不设置默认值。这在逻辑上比定义“垃圾”要好。

          如果您想部分分配对象:
          您可以使用可选属性定义类型,例如:

          interface IX {
              a: string,
              b?: any,
              c?: AnotherType
          }
          

          在这种情况下,您只需设置a。其他类型标有?,表示它们是可选的,默认值为undefined

          甚至

          let x: Partial<IX> = { ... }
          

          这使得所有字段都是可选的。

          在任何情况下,您都可以使用 undefined 作为默认值,这取决于您的用例

          【讨论】:

            【解决方案6】:

            接口的默认值是不可能的,因为接口只在编译时存在。

            替代方案:

            您可以为此使用工厂方法,该方法返回一个实现 XI 接口的对象。

            示例:

            class AnotherType {}
            
            interface IX {
                a: string,
                b: any,
                c: AnotherType | null
            }
            
            function makeIX (): IX {
                return {
                a: 'abc',
                b: null,
                c: null
                }
            }
            
            const x = makeIX();
            
            x.a = 'xyz';
            x.b = 123;
            x.c = new AnotherType();
            

            我对您的示例所做的唯一更改是将属性 c 设为 AnotherType | null。这将是没有任何编译器错误所必需的(如果您将 null 初始化为属性 c,此错误也出现在您的示例中)。

            【讨论】:

              【解决方案7】:

              我可以告诉界面将我不提供的属性默认为 null 吗?什么会让我这样做

              没有。您不能为接口或类型别名提供默认值,因为它们只是编译时的,默认值需要运行时支持

              另类

              但未指定的值在 JavaScript 运行时默认为 undefined。所以你可以将它们标记为可选:

              interface IX {
                a: string,
                b?: any,
                c?: AnotherType
              }
              

              现在当你创建它时,你只需要提供a:

              let x: IX = {
                  a: 'abc'
              };
              

              您可以根据需要提供值:

              x.a = 'xyz'
              x.b = 123
              x.c = new AnotherType()
              

              【讨论】:

              • 谢谢,没有定义一堆默认值,我曾经无法为未定义的属性设置值
              • 使用 any 会破坏 TypeScript 的目的。还有其他答案没有这个缺点。
              • 任何人都可以帮助我解决类似的问题,但使用泛型。 Here at this question
              • 奇怪的是 basarat 会使用“任何”示例,因为在提供的链接中,他提供了一个更好的选择,“让 foo = {} as Foo;” (“Foo”是一个接口)
              • OP 在创建 TypeScript 接口时遇到了麻烦,正在寻求解决方案来解决问题。您的解决方案是完全放弃接口?不妨建议也跳过 TypeScript...
              【解决方案8】:

              您可以使用两个单独的配置。一个作为具有可选属性的输入(将具有默认值),另一个仅具有必需的属性。这可以通过&amp;Required 来实现:

              interface DefaultedFuncConfig {
                b?: boolean;
              }
              
              interface MandatoryFuncConfig {
                a: boolean;
              }
              
              export type FuncConfig = MandatoryFuncConfig & DefaultedFuncConfig;
               
              export const func = (config: FuncConfig): Required<FuncConfig> => ({
                b: true,
                ...config
              });
              
              // will compile
              func({ a: true });
              func({ a: true, b: true });
              
              // will error
              func({ b: true });
              func({});
              

              【讨论】:

                【解决方案9】:

                我的解决方案:

                我在 Object.assign 上创建了包装器来修复打字问题。

                export function assign<T>(...args: T[] | Partial<T>[]): T {
                  return Object.assign.apply(Object, [{}, ...args]);
                }
                

                用法:

                env.base.ts

                export interface EnvironmentValues {
                export interface EnvironmentValues {
                  isBrowser: boolean;
                  apiURL: string;
                }
                
                export const enviromentBaseValues: Partial<EnvironmentValues> = {
                  isBrowser: typeof window !== 'undefined',
                };
                
                export default enviromentBaseValues;
                

                env.dev.ts

                import { EnvironmentValues, enviromentBaseValues } from './env.base';
                import { assign } from '../utilities';
                
                export const enviromentDevValues: EnvironmentValues = assign<EnvironmentValues>(
                  {
                    apiURL: '/api',
                  },
                  enviromentBaseValues
                );
                
                export default enviromentDevValues;
                

                【讨论】:

                  【解决方案10】:

                  您可以按照文档中的说明使用 Partial 映射类型: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html

                  在您的示例中,您将拥有:

                  interface IX {
                      a: string;
                      b: any;
                      c: AnotherType;
                  }
                  
                  let x: Partial<IX> = {
                      a: 'abc'
                  }
                  

                  【讨论】:

                  • 如果x是具有默认值的对象,那么创建最终实例对象let a: IX = Object.assign({b:true}, x);会导致错误Partial&lt;IX&gt; cannot be assigned to IX
                  • 部分更改类型。 x 不再实现IX,而是IX 的一部分。 Partial 适用于每个属性都可能是可选的地方,例如使用 ORM,您可以在其中传递对象接口的一部分并仅更新已定义的字段(而不是 undefined,这是每个字段Partial 可以)。对于具有默认值字段的接口,您可以使用let x: Partial&lt;IX&gt; = { /* non-default fields */ } as IX 语法声明实现这些类型接口的对象字面量,而无需声明默认值。
                  • x 没有实现IX,但您仍然可以使用它创建一个使用默认值实现IX 的对象。您需要为所有必需的值提供默认值,否则无法保证实现IX。例如。 const defaults: IX = { a: 'default', b: {}, c: new AnotherType() }; const y = { ...defaults, ...x };
                  【解决方案11】:

                  我在寻找比我所得到的更好的方法时偶然发现了这一点。阅读答案并尝试后,我认为值得发布我正在做的事情,因为其他答案对我来说并不简洁。每次设置新界面时,我只需要编写少量代码,这对我来说很重要。我决定了...

                  使用自定义的通用 deepCopy 函数:

                  deepCopy = <T extends {}>(input: any): T => {
                    return JSON.parse(JSON.stringify(input));
                  };
                  

                  定义你的界面

                  interface IX {
                      a: string;
                      b: any;
                      c: AnotherType;
                  }
                  

                  ...并在单独的 const 中定义默认值。

                  const XDef : IX = {
                      a: '',
                      b: null,
                      c: null,
                  };
                  

                  然后像这样初始化:

                  let x : IX = deepCopy(XDef);
                  

                  这就是我们所需要的一切......

                  .. 然而..

                  如果您想自定义初始化任何根元素,您可以修改 deepCopy 函数以接受自定义默认值。函数变为:

                  deepCopyAssign = <T extends {}>(input: any, rootOverwrites?: any): T => {
                    return JSON.parse(JSON.stringify({ ...input, ...rootOverwrites }));
                  };
                  

                  然后可以这样调用:

                  let x : IX = deepCopyAssign(XDef, { a:'customInitValue' } );
                  

                  任何其他首选的深拷贝方式都可以。如果只需要一个浅拷贝,那么 Object.assign 就足够了,不需要实用程序 deepCopydeepCopyAssign 函数。

                  let x : IX = object.assign({}, XDef, { a:'customInitValue' });
                  

                  已知问题

                  • 它不会以这种形式进行深度分配,但并不难 修改 deepCopyAssign 以在分配前迭代和检查类型。
                  • 解析/字符串化过程将丢失函数和引用。 我的任务不需要这些,OP 也不需要。
                  • IDE 不会提示自定义初始化值,也不会在执行时检查类型。

                  【讨论】:

                    【解决方案12】:

                    虽然@Timar 的答案非常适合null 默认值(所要求的),但这里还有另一个允许其他默认值的简单解决方案:定义一个选项接口以及一个包含默认值的相应常量;在构造函数中使用spread operator 设置options 成员变量

                    interface IXOptions {
                        a?: string,
                        b?: any,
                        c?: number
                    }
                    
                    const XDefaults: IXOptions = {
                        a: "default",
                        b: null,
                        c: 1
                    }
                    
                    export class ClassX {
                        private options: IXOptions;
                    
                        constructor(XOptions: IXOptions) {
                            this.options = { ...XDefaults, ...XOptions };
                        }
                    
                        public printOptions(): void {
                            console.log(this.options.a);
                            console.log(this.options.b);
                            console.log(this.options.c);
                        }
                    }
                    

                    现在你可以像这样使用这个类了:

                    const x = new ClassX({ a: "set" });
                    x.printOptions();
                    

                    输出:

                    set
                    null
                    1
                    

                    【讨论】:

                    • 这不能用作每种情况的默认解决方案,但对于某些情况绝对是一种创造性的解决方案。 ?
                    • 如果不是只记录我想使用它们的值,比如 const calcvalue = this.options.a * 1000;这仍然会引起警报,因为它可能是未定义的..
                    • @ManishRawat 例如,您可以不将任何属性声明为可选,而是让构造函数采用Partial&lt;IXOptions&gt; 类型。这样 TS 就知道所有属性都将存在于 this.options 中,但构造函数中不需要。
                    • @Thor84no 虽然 Partial 的定义是让一切都是可选的,但这就是它所做的一切
                    • @JacksonHaenchen 将部分作为构造函数的参数与返回未列为部分的对象不同。此处的构造函数将首先将所有内容分配为默认值,然后将其作为输入值(如果提供)。因此,输入值可以是部分的,而不会影响创建的对象是否完整和类型安全。
                    【解决方案13】:

                    您不能在界面中设置默认值,但您可以通过使用可选属性来完成您想做的事情(比较第 3 段):

                    https://www.typescriptlang.org/docs/handbook/interfaces.html

                    只需将界面更改为:

                    interface IX {
                        a: string,
                        b?: any,
                        c?: AnotherType
                    }
                    

                    你可以这样做:

                    let x: IX = {
                        a: 'abc'
                    }
                    

                    如果未设置这些属性,请使用您的 init 函数将默认值分配给 x.bx.c

                    【讨论】:

                    • 在问题中要求使用null 初始化x.bx.c。当写let x = {a: 'abc'} 时,x.bx.cundefined,所以这个答案并不完全符合要求,虽然它是一个聪明的快速修复。
                    • @BennyNeugebauer 接受的答案有同样的缺陷。这是最好的答案
                    • 如果 x 是具有默认值的对象,则创建最终实例对象 let a: IX = Object.assign({b:true}, x); 将导致 b,c 在实例对象中也是可选的,这可能是不希望的
                    • 属性不应标记为“可选”,以方便使用该界面的人。答案是“不”,您不能“告诉接口提供默认值”,但您可以提供工厂方法来初始化接口的实例。
                    【解决方案14】:

                    你可以用一个类来实现接口,然后你可以在构造函数中处理初始化成员:

                    class IXClass implements IX {
                        a: string;
                        b: any;
                        c: AnotherType;
                    
                        constructor(obj: IX);
                        constructor(a: string, b: any, c: AnotherType);
                        constructor() {
                            if (arguments.length == 1) {
                                this.a = arguments[0].a;
                                this.b = arguments[0].b;
                                this.c = arguments[0].c;
                            } else {
                                this.a = arguments[0];
                                this.b = arguments[1];
                                this.c = arguments[2];
                            }
                        }
                    }
                    

                    另一种方法是使用工厂函数:

                    function ixFactory(a: string, b: any, c: AnotherType): IX {
                        return {
                            a: a,
                            b: b,
                            c: c
                        }
                    }
                    

                    那么你可以简单地:

                    var ix: IX = null;
                    ...
                    
                    ix = new IXClass(...);
                    // or
                    ix = ixFactory(...);
                    

                    【讨论】:

                      猜你喜欢
                      • 2018-12-13
                      • 2022-11-07
                      • 2019-12-27
                      • 1970-01-01
                      • 2013-09-09
                      • 2022-11-15
                      • 1970-01-01
                      • 1970-01-01
                      • 2020-03-27
                      相关资源
                      最近更新 更多