简体   繁体   中英

Stricter type assertion in Typescript

So lets's say I have

declare function doSomething(...args: any[]): any

interface Example {
    a: number
    b: number
}

doSomething({a: 2, b: 1, c: 10} as Example)

This does not report an error as that object extends Example and typescript is happy, so I end up using an ugly identity function:

function cast<T>(arg: T) {
    return arg
}

doSomething(cast<Example>({a: 2, b: 1, c: 10})) // yay, error

It irritates the heck out of me that I need to actually call an %^#% no-op function just to do a proper type cast. I end up declaring it in every file that needs it just to give the js compiler better chances of optimizing it out.

Is there any ts magic I don't know about that can avoid the function call?

And yes, I know I can do this:

const x: Example = {a: 2, b: 1, c: 10}
doSomething(x)

and this:

declare function doSomething(arg: Example): any

that's really not the point here. Consider the following lambda:

const example = (i: number, j: number, k: number) => cast<Example>({a: 1, b: 2, c: 3})

to properly set the type without resulting to the identity function I would need to write

const example: (i: number, j: number, k: number) => Example = (i, j, k) => ({a: 1, b: 2, c: 3})

which is no very DRY

And yes, I can just write

function example (i: number, j: number, k: number): Example {
    return {a: 1, b: 2, c: 3}
}

again, not the point

// edit

as @thedude just blew my mind in the comment with the lambda return typeing syntax I did not know about, one more example where I use this casting

declare function doSomethingWithArray(arg: Example[]): void

doSomethingWithArray(cast<(Example | boolean)[]>([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3}
]).filter(x => x) as Example[])

// edit 2

as it seems I ma terrible at explaining what I want, another example: this generic function solves the filter example-problem

function filterFalse<T>(x: (T | false)[]) {
    return x.filter(x => x) as Exclude<T, false>[]
}

doSomethingWithArray(filterFalse<Example>([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3} // error
]))

but requires defining a specialized function just for this specific task. I am asking if there is a generic way to force a strict type check on a compile-time literal without resulting to a function call. So exactly what cast<T> does but without the pointless call in the js output.

I don't see why you need a cast function in any of these example. I think you may be overthinking this.


Let's start here, which is perfectly fine as is.

doSomething({a: 2, b: 1, c: 10} as Example)

The as keyword is still generally typesafe. It lets you cast a value to supertype. In this case { a: number, b: number } is a supertype of { a: number, b: number, c: number } .

For example:

const value = { a: 1, b: 2, c: 2 }
const exampleValue: { a: number, b: number } = value // works!

And in your first example, if you omit a required property you will have a type error:

doSomething({b: 1, c: 10} as Example)
// Property 'a' is missing in type '{ b: number; c: number; }'
// but required in type 'Example'.(2352)

So then why does this provide an error?

cast<Example>({a: 1, b: 2, c: 3}) // error

Because when you create a value and assign it to a supertype, then you're only reference to that value will be of that supertype. Meaning whatever other properties may exist would be inaccessible. Example does not have a c property. So typescript assumes that's a mistake, and rightly so.

But in real code, if you passed an object with a, b, and c to a function that only expect a and b, then nothing bad would happen. You've satisfied the constraints, so there's no problem.


In this snippet:

doSomething(cast<Example>({a: 2, b: 1, c: 10})) // yay, error

The cast is pointless. If doSomething cares about the type it receives, then it should declare that in its arguments. And if it doesn't care, then there is no need for a cast at all.


const x: Example = {a: 2, b: 1, c: 10}
doSomething(x)

This is basically the same as as Example from above.


declare function doSomething(arg: Example): any

This is correct, and exactly what you should be doing. If the function takes a specific type, let it enforce that on its own. You shouldn't need to cast anything if you are assigning it to any , since that cast will be lost anyway.


const example = (i: number, j: number, k: number) =>
  cast<Example>({a: 1, b: 2, c: 3})

If you want a function to return a type, you typically just annotate the return type. The following works fine:

const example(i: number, j: number, k: number): Example =>
  ({ a: i, b: j, c: k }) // error, as expected

To properly set the type without resulting to the identity function I would need to write:

const example: (i: number, j: number, k: number) => Example = (i, j, k) => ({a: 1, b: 2, c: 3})

Incorrect, see previous example. One function, type is enforced as expected.


Lastly:

function example (i: number, j: number, k: number): Example {
    return {a: 1, b: 2, c: 3} // error as expected.
}

If you want a function that creates and returns an Example , this is perfect. It properly alerts you that you have a property that could never be ever be used, and clearly documents the return value.


TLDR:

None of your code snippets should require a cast<T>() function at all. I think your best bet is eliminate all occurrences of any and then just let Typescript enforce the types. That's what it's there for. The compiler is pretty good, and doesn't typically need that much help.


Filtering

Filtering is indeed a weird case in all this. The problem stems from the fact that filter() is typed to return the exact same array type that it started with, regardless of what you filter. The common solution is to make the filter callback function a type predicate function

Then you could do something like:

interface Example {
    a: number
    b: number
}

declare function doSomethingWithArray(arg: Example[]): void

doSomethingWithArray([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3} // no error because of perfectly safe casting to supertype
].filter((x): x is Example => typeof x !== 'boolean'))

See playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM