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)