Typescript interface default values

Typescript

Typescript Problem Overview


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 '{}'.

Typescript Solutions


Solution 1 - Typescript

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

Solution 2 - Typescript

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.

Solution 3 - Typescript

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

Solution 4 - Typescript

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

Solution 5 - Typescript

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'
}

Solution 6 - Typescript

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 }

Solution 7 - Typescript

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

Solution 8 - Typescript

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:

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

Solution 9 - Typescript

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.

Solution 10 - Typescript

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;

Solution 11 - Typescript

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

Solution 12 - Typescript

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.

Solution 13 - Typescript

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.

Solution 14 - Typescript

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

Solution 15 - Typescript

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?"

Solution 16 - Typescript

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiond512View Question on Stackoverflow
Solution 1 - TypescriptbasaratView Answer on Stackoverflow
Solution 2 - TypescriptTimarView Answer on Stackoverflow
Solution 3 - TypescriptJack MillerView Answer on Stackoverflow
Solution 4 - TypescriptNitzan TomerView Answer on Stackoverflow
Solution 5 - TypescriptF. BauerView Answer on Stackoverflow
Solution 6 - TypescriptvitaletsView Answer on Stackoverflow
Solution 7 - TypescriptWillem van der VeenView Answer on Stackoverflow
Solution 8 - Typescriptor sofferView Answer on Stackoverflow
Solution 9 - TypescriptMoss PalmerView Answer on Stackoverflow
Solution 10 - TypescriptLuckylookeView Answer on Stackoverflow
Solution 11 - TypescriptSteven VachonView Answer on Stackoverflow
Solution 12 - TypescriptnrofisView Answer on Stackoverflow
Solution 13 - TypescriptJose QuijadaView Answer on Stackoverflow
Solution 14 - TypescriptgarlicbreadView Answer on Stackoverflow
Solution 15 - TypescriptTim MouskhelichviliView Answer on Stackoverflow
Solution 16 - TypescriptMax BaldwinView Answer on Stackoverflow