TypeScript type cast

前端 未结 3 589
孤城傲影
孤城傲影 2021-01-24 12:47

I have an object, and I need to add some extra properties to it in some cases.

interface Item {
    name: string
}

functi         


        
相关标签:
3条回答
  • 2021-01-24 13:16

    1. Union type approach

    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


    2. Function as data transformer

    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.

    0 讨论(0)
  • 2021-01-24 13:26

    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.

    0 讨论(0)
  • 2021-01-24 13:33

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

    0 讨论(0)
提交回复
热议问题