I have an object, and I need to add some extra properties to it in some cases.
interface Item {
name: string
}
functi
What I think you want to do, is to determine some property relations. As it is wrong to just get different type and cast it as it would be another one. I think your Item
type can have some additional properties in some circumstances, and that we should model in the type definition!
Lets start from proper typing:
type ItemNoFoo = { name: string };
type ItemWithFoo = ItemNoFoo & {foo: string}
type Item = ItemNoFoo | ItemWithFoo;
I created Item as a union of ItemNoFoo
and ItemWithFoo
. Thanks that we can define two different states of our object.
Now I will create a function, guard function which will check if we are ItemNoFoo
or ItemWithFoo
states.
const hasFoo = (item: Item): item is ItemWithFoo => item.name === 'test';
Ok great so we can now ask if Item
has property foo or not in scope of one type ( as our type is just a union of two other types ).
The final code:
type ItemNoFoo = { name: string };
type ItemWithFoo = ItemNoFoo & {foo: string}
type Item = ItemNoFoo | ItemWithFoo;
const hasFoo = (item: Item): item is ItemWithFoo => item.name === 'test';
function addProp(obj: Item) {
if (hasFoo(obj)) {
obj.foo = 'hello'; // in if block obj is inferenced as ItemWithFoo!
}
}
Some more info about this approach you can find here - Sum types
If it happens that our function needs to create a new structure, so is like a - data creator, or data transformer, then we should look on it as in -> out
pipe. In types, in is ItemNoFoo
and out type is ItemWithFoo
| ItemWithFoo
( types definitions in point 1 ).
function transform(obj: ItemNoFoo): Item {
if (obj.name === 'test') {
return {
...obj,
foo: 'hello'
} // here we create new value with new type
} else {
return obj;
}
}
As you can see still union type Item
is handy as this function returns or unchanged ItemNoFoo
type instance or ItemWithFoo
.
Lets check the using of it:
function f(item: ItemNoFoo) {
const finalItem = transform(item); // type of finalItem is ItemWithFoo | ItemNoFoo
}
If further you want to be sure if you deal with one or another, then handy can be hasFoo
guard function (defined in solution 1) which will determine which of these two types the value is.
obj = obj as WithFoo
does not work because the type of obj
is Item
and you want to store in it an object of type WithFoo
.
But you can use another variable of type WithFoo
and store obj
in it. Because obj
is an object, both variables will keep references to the same data:
interface Item {
name: string
}
function addProp(obj: Item) {
type WithFoo = Item & { foo?: string; }
const foo = obj as WithFoo;
if (obj.name == 'test') {
foo.foo = 'hello';
}
}
const x1: Item = { name: 'test' }
addProp(x1);
console.log(x1);
const x2: Item = { name: 'not-matching' }
addProp(x2);
console.log(x2);
Try it online.
1.) Why does obj = obj as WithFoo
not work? Is there any way to cast the type, without introducing another variable?
First of all the obj
function parameter variable is declared with an Item
type, so TS does not know that obj
contains the type { foo?: string}
. Your second chance would be the compiler's control flow analysis for assignments, which in this case cannot narrow the obj
type to WithFoo
, as obj
does not have a union type:
An assignment (including an initializer in a declaration) of a value of type S to a variable of type T changes the type of that variable to T narrowed by S in the code path that follows the assignment.
The type T narrowed by S is computed as follows:
If T is not a union type, the result is T.
If T is a union type, the result is the union of each constituent type in T to which S is assignable.
That is the reason, why you get the error Property 'foo' does not exist on type 'Item'
in the example. Instead the following code would properly narrow and compile:
type WithFoo = { foo?: string; }
function addProp(obj: Item | WithFoo) {
obj = obj as WithFoo;
obj.foo = 'hello';
}
If you don't want to do re-assignments or introduce another variable, foo
property can be accessed with an inline type assertion, which in general is usable on all JavaScript expressions:
(obj as WithFoo).foo = 'hello';
Having said that, a more safe way is probably to assume obj
to be a union type Item | WithFoo
and use type guards instead of a hard cast (see @Maciej Sikora's answer).
2.) Why does const anotherObj = obj as AnotherType
work?
When you declare a new variable like const anotherObj = obj as AnotherType
, the compiler automatically infers the type of variable anotherObj
to AnotherType
. TS does additional checks to make sure, AnotherType
is compatible to typeof obj
. E.g. this wouldn't compile:
function addProp(obj: Item) {
const anotherObj = obj as string // error (OK)
// ...
}
3.) Can a variable's type be changed after its declaration?
Nope, let
and const
variables cannot be redeclared (var with same type, but not important here), which implies, the declared variable types also cannot be changed. The variables could be narrowed via control flow analysis though, see 1.).