Home > Net >  Why does Typescript not like it when I use a simple object for an interface that has all optional pr
Why does Typescript not like it when I use a simple object for an interface that has all optional pr

Time:03-01

I am trying to provide a mock for uirouter's StateService... I am simply doing:

beforeEach(() => {
  stateService = jasmine.createSpyObj('StateService', ['go']) as StateService;
}

...

it('calls go', () => {
  // ...
  let options = { location: 'replace', inherit: true };
  expect(stateService.go).toHaveBeenCalledWith('foo', params, options);
});

I get:

error TS2345: Argument of type '{ location: string; inherit: boolean; }' is not assignable to parameter of type 'Expected'. Type '{ location: string; inherit: boolean; }' is not assignable to type '{ location?: ExpectedRecursive<boolean | "replace">; relative?: ExpectedRecursive<string | StateObject | StateDeclaration>; ... 8 more ...; source?: ExpectedRecursive<...>; }'. Types of property 'location' are incompatible. Type 'string' is not assignable to type 'ExpectedRecursive<boolean | "replace">'. expect(stateService.go).toHaveBeenCalledWith('foo', params, options);

Yet if I do it all inline without a variable assignment:

it('calls go', () => {
  // ...
  expect(stateService.go).toHaveBeenCalledWith('foo', params, { location: 'replace', inherit: true });
});

Then it works fine...

uirouter's TransitionOptions interface is defined like:

export interface TransitionOptions {
    location?: boolean | 'replace';
    relative?: string | StateDeclaration | StateObject;
    inherit?: boolean;
    notify?: boolean;
    reload?: boolean | string | StateDeclaration | StateObject;
    custom?: any;
    supercede?: boolean;
    reloadState?: StateObject;
    redirectedFrom?: Transition;
    current?: () => Transition;
    source?: 'sref' | 'url' | 'redirect' | 'otherwise' | 'unknown';
}

Where everything is optional... Why does Typescript not like me assigning this to a simple object as a variable?

CodePudding user response:

The type of this line:

let options = { location: 'replace', inherit: true };

is

options: { location: string, boolean: true }

This is because the type of 'replace' is expanded to string. Because of this, string is too wide to assign to location which is expected to be 'replace' | boolean | undefined.

To tell Typescript that 'replace' is not a string and should only be replace, use as const.

let options = { location: 'replace' as const, inherit: true };

CodePudding user response:

This is because when you declare by inferring the type of the variable from the assigned value, it will generalize the value of the location property to string, making it invalid for the replace value, as replace will just be the property value, not its type.

To fix this you can infer the type of the variable in the declaration, like this:

function foo(param: TransitionOptions) {
    console.log('foo', param)
}

foo({ location: 'replace', inherit: true })

let bar: TransitionOptions = { location: 'replace', inherit: true }
foo(bar)

CodePudding user response:

Let's distil this to a simpler example with the same error.

function foo(v: "replace") {
}

foo("replace"); // This works

const t1 = "replace";
foo(t1); // This also works

let t2 = "replace";
foo(t2);  // Compiler error 

let t3:"replace" = "replace";
foo(t3)

If you declare a variable and assign it a string value, typescript will assume it is of type string, not a string literal type.

Applying it to your options example

let options1 = { 
    location: 'replace',  // This is a mutable string, not a string literal!
    inherit: true 
};

foo(options1.location); // << compiler error

let options2: {location:'replace', inherit: boolean} = { 
    location: 'replace', // String literal, because we declared the type
    inherit: true 
};

foo(options2.location); // << This works!

You've stumbled upon interesting behaviour with regard to typescript choosing between a string type and a string literal type. Here's some more interesting cases.

function foo(v: "replace") {
}

function bar(p: {location: 'replace'}) {
}

let options = {
    location: 'replace'
}

bar(options); // Compilation error
foo(options.location); // Also a compilation error

bar({ location: 'replace'}); // No error.

foo( { location: 'replace'}.location); // But the compiler calls this an error! (I don't think it should)





  • Related