ChatGPT解决这个技术问题 Extra ChatGPT

Typescript interface default values

I have the following interface in TypeScript:

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

I declare a variable of that type and I initialize all the properties

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

Then I assign real values to them in an init function later

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

But I don't like having to specify a bunch of default null values for each property when declaring the object when they're going to just be set later to real values. Can I tell the interface to default the properties I don't supply to null? What would let me do this:

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

without getting a compiler error. Right now it tells me

TS2322: Type '{}' is not assignable to type 'IX'. Property 'b' is missing in type '{}'.

IMO, the answer stackoverflow.com/a/35074490/129196 shouldn't be the approach to take. If you can have an object in a state without having all its properties initialized and still be valid, then you should declare those properties as optional as indicated by this answer: stackoverflow.com/a/43226857/129196. Otherwise we will lose the sole purpose of using typescript (for type safety).

b
basarat

Can I tell the interface to default the properties I don't supply to null? What would let me do this

No. You cannot provide default values for interfaces or type aliases as they are compile time only and default values need runtime support

Alternative

But values that are not specified default to undefined in JavaScript runtimes. So you can mark them as optional:

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

And now when you create it you only need to provide a:

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

You can provide the values as needed:

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

Thank you , without defining bunch of default values i used to have unable to set value for undefined property
Using any undermines the purpose of TypeScript. There are other answers without this drawback.
could any one help me about similar question but using generics. Here at this question
Odd that basarat would go with the 'any' example when, in the link provided, he offers a much better option with 'let foo = {} as Foo;' ('Foo" being an Interface)
The OP has gone to the trouble of creating a TypeScript interface and is asking for solutions to the boiler plating. Your solution is to forgo Interfaces completely? May as well suggest that skips TypeScript too...
T
Timar

You can't set default values in an interface, but you can accomplish what you want to do by using Optional Properties (compare paragraph #3):

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

Simply change the interface to:

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

You can then do:

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

And use your init function to assign default values to x.b and x.c if those properies are not set.


In the question it was asked to initialize x.b and x.c with null. When writing let x = {a: 'abc'} then x.b and x.c are undefined, so this answer doesn't fully meet the requirements, although it's a smart quick fix.
@BennyNeugebauer The accepted answer has the same flaw. This is the best answer
if x is object with default values, then creating final instance object let a: IX = Object.assign({b:true}, x); will cause b,c to be optional also in instance object, which may not be desired
Properties should not be marked "optional" for the convenience of those using the interface. The answer is "no" you cannot "tell the interface to supply defaults" but you can provide a factory method to initialize an instance of an interface.
o
oluckyman

While @Timar's answer works perfectly for null default values (what was asked for), here another easy solution which allows other default values: Define an option interface as well as an according constant containing the defaults; in the constructor use the spread operator to set the options member variable

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);
    }
}

Now you can use the class like this:

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

Output:

set
null
1

This cant be used as a default solution for every case, But definitely a creative solution for some cases. 👍
what if instead of just logging the values I want to use them like const calcvalue = this.options.a * 1000; this would still raise alarm as it can be potentially undefined..
@ManishRawat You can for example not declare any of the properties as optional, but instead let the constructor take the type Partial<IXOptions>. This way TS knows all properties will be present in this.options, but none are required in the constructor.
@Thor84no though the definition of Partial is to make everything optional, that's all it does
@JacksonHaenchen taking a partial as an argument for a constructor is not the same as returning an object that isn't listed as a partial. The constructor here will assign everything as defaults first and then as the input values if provided. The input values can thus be partial without affecting whether the created object is complete and typesafe.
N
Nitzan Tomer

You can implement the interface with a class, then you can deal with initializing the members in the constructor:

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];
        }
    }
}

Another approach is to use a factory function:

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

Then you can simply:

var ix: IX = null;
...

ix = new IXClass(...);
// or
ix = ixFactory(...);

F
F. Bauer

You can use the Partial mapped type as explained in the documentation: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html

In your example, you'll have:

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

let x: Partial<IX> = {
    a: 'abc'
}

if x is object with default values, then creating final instance object let a: IX = Object.assign({b:true}, x); will cause error Partial<IX> cannot be assigned to IX
Partial changes the type. x no longer implements IX, but a partial of IX. Partial is good for places where every property might be optional, for example with an ORM, where you can pass a partial of an object interface and update only the fields that are defined (as opposed to undefined which is what every field of a Partial can be). For interfaces that have fields with default values, you can declare object literals implementing those type of interfaces, without having to declare the default values, using the let x: Partial<IX> = { /* non-default fields */ } as IX syntax.
x doesn't implement IX, but you can still use it to create an object that implements IX by using defaults. You need to provide defaults for all required values though, otherwise there's no guarantee of implementing IX. E.g. const defaults: IX = { a: 'default', b: {}, c: new AnotherType() }; const y = { ...defaults, ...x };
v
vitalets

I use the following pattern:

Create utility type 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>>>

Declare class with options/defaults:

// 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);
    }
}

Create class instance:

const myClass = new MyClass({
    a: 'hello',
    b: true,
});

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

W
Willem van der Veen

Default values to an interface are not possible because interfaces only exists at compile time.

Alternative solution:

You could use a factory method for this which returns an object which implements the XI interface.

Example:

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();

The only thing I changed with regard to your example is made the property c both AnotherType | null. Which will be necessary to not have any compiler errors (This error was also present in your example were you initialized null to property c).


o
or soffer

It's best practice in case you have many parameters to let the user insert only few parameters and not in specific order.

For example, bad practice:

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

Since you have to supply all the parameters before the one you actually want (d).

Good practice to use is:

foo({d=3})

The way to do it is through interfaces. You need to define the parameter as an interface like:

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

And define the function like:

foo(arguments: Arguments)

Now interfaces variables can't get default values, so how do we define default values?

Simple, we define default value for the whole interface:

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

Now if the user pass:

foo({d=3})

The actual parameters will be:

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

Another option without declaring an interface is:

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

Follow Up: In the previous function definition we define defaults for the fields of the parameter object, but not default for the object itself. Thus we will get an extraction error (e.g. Cannot read property 'b' of undefined) from the following call:

foo()

There are two possible solutions:

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) 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.. }


M
Moss Palmer

I stumbled on this while looking for a better way than what I had arrived at. Having read the answers and trying them out I thought it was worth posting what I was doing as the other answers didn't feel as succinct for me. It was important for me to only have to write a short amount of code each time I set up a new interface. I settled on...

Using a custom generic deepCopy function:

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

Define your interface

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

... and define the defaults in a separate const.

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

Then init like this:

let x : IX = deepCopy(XDef);

That's all that's needed..

.. however ..

If you want to custom initialise any root element you can modify the deepCopy function to accept custom default values. The function becomes:

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

Which can then be called like this instead:

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

Any other preferred way of deep copy would work. If only a shallow copy is needed then Object.assign would suffice, forgoing the need for the utility deepCopy or deepCopyAssign function.

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

Known Issues

It will not deep assign in this guise but it's not too difficult to modify deepCopyAssign to iterate and check types before assigning.

Functions and references will be lost by the parse/stringify process. I don't need those for my task and neither did the OP.

Custom init values are not hinted by the IDE or type checked when executed.


L
Luckylooke

My solution:

I have created wrapper over Object.assign to fix typing issues.

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

Usage:

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;

S
Steven Vachon

You could use two separate configs. One as the input with optional properties (that will have default values), and another with only the required properties. This can be made convenient with & and 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({});

n
nrofis

It is depends on the case and the usage. Generally, in TypeScript there are no default values for interfaces.

If you don't use the default values
You can declare x as:

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

Then, in your init function you can set real values:

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

In this way, x can be undefined or defined - undefined represents that the object is uninitialized, without set the default values, if they are unnecessary. This is loggically better than define "garbage".

If you want to partially assign the object: You can define the type with optional properties like:

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

In this case you have to set only a. The other types are marked with ? which mean that they are optional and have undefined as default value.

Or even

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

Which makes all the fields optional.

In any case you can use undefined as a default value, it is just depends on your use case.


J
Jose Quijada

You can also have a helper method/function that returns the object with default property values, and then the calling code can override the defaults as needed. That's the approach I'm following as I've been faced with the same question in my current project. This way coding the default property value object is a one time affair, and you can reuse this object throughout your entire application.


g
garlicbread

Another way is to use https://www.npmjs.com/package/merge
This is the same as the last answer but a bit tidier.

Lets install merge

yarn add -D merge

Next lets create an interface with some options. We'll place this into ./types/index.ts

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

Next lets create a set of defaults you could put this in the same file, but lets keep the types seperate and place it into ./config/index.ts

import { ExampleOpts } from '../types'

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

Next lets join it all together with a function within ./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)
}

T
Tim Mouskhelichvili

Another way is to use the Pick utility type and choose the properties you wish to set to required.

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

let x: Pick<IX, 'a'> = {
    a: 'abc'
}

Then when you want to declare the real IX object, you just merge the defaults with new values, like so:

const newX: IX = {
    ...x,
    b: 'b',
    c: () => {}
}

This answer was taken from "How To Set Up A TypeScript Interface Default Value?"


M
Max Baldwin

Working through this now. Use a class instead of an interface.

class IX {
  a: String = '';
  b?: any;
  c: Cee = new Cee();
}

class Cee {
  c: String = 'c';
  e: String = 'e';
}

p
papierkorp

I needed this for a React Component.

You can use the Nullish Coalescing Operator which will assign a default Value if the left Hand Value is Null or Undefined:

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

const ixFunction: React.FC<IX> = (props) => {
  console.log(props.b?? "DefaultValue")
}

But this only works if you want to use the variable in only one place.