2

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.

10
  • 1
    you can also write: const example = (i: number, j: number, k: number): Example => ({a: 1, b: 2, c: 3}) Commented Jul 11, 2021 at 20:41
  • What's wrong with the last snippet? Why is that not the point? If you write code that doesn't use any, then you should very rarely need to do casting like this. Commented Jul 11, 2021 at 21:29
  • the point was I wanted a lambda declaration not a function declaration, that is indeed solved with what @thedude wrote in comment above, see edit for another example Commented Jul 11, 2021 at 22:43
  • @OtisVallone In the new snippet, you still don't need the cast function. The filter function can act on any array, so you can give it an array of mixed booleans and Example objects. Typescript will already infer that the type of that array is (boolean | Example)[]. Commented Jul 11, 2021 at 22:55
  • @siride Filter infers an array of union of literal objects (and boolean), not (boolean | Example)[]. Besides for some unknown to me reason even this doSomethingWithArray([{a: 1, b: 2, c: 3}].filter(x => x)) does not fail on the c, while without the filter it does. Commented Jul 11, 2021 at 23:12

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

4 Comments

the first snippet where to explain what I want, the second two where to explain what I don't want, the lambda was the only real example where I use it. I did not know about the lambda syntax where you can specify the return type directly, that solves the lambda example, I added another example where I use this kind of casting.
See the end of my answer for an update on filtering. You can use a type predicate function to tell typescript that your filtering operation changes the type of the array.
Look, I made typos in optional object fields too many times to agree with you that it is perfectly safe. Basically you are suggesting that every interface with all fields optional is as good as Record<string, any> (well, not exactly because it will still check the value types of the existing keys, but I hope you get the picture). Also if it's such an no error then why is it an error when you just pass a raw array, not the filtered version? The value can be safely casted to the parameter type as its a supertype, yet the compiler still reports an error (as it should).
doSomething({b: 1, c: 10} as Example) is only valid because c is not exptected so no type overlaps the other. If you do doSomething({b: 1} as Example) would be valid for the compiler

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.