microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.47k stars 12.42k forks source link

Safe type assertion operator #56235

Open bgenia opened 11 months ago

bgenia commented 11 months ago

🔍 Search Terms

as, satisfies, type assertion, type casting

✅ Viability Checklist

⭐ Suggestion

I suggest adding a safe type assertion operator, a kind of middle ground between current as and satisfies.

Here's what it might look like:

type Foo = { a: number }

const foo1 = { a: 1 } as! Foo // OK. foo1: Foo

const foo2 = { a: 1, b: 2 } as! Foo // OK, foo2: Foo

const foo3 = { b: 2 } as! Foo // Error, { b: number } is not assignable to Foo

const foo4 = { } as! Foo // Error, {} is not assignable to Foo

📃 Motivating Example

For example, such operator will allow to safely give named types to expressions without declaring extra variables or helper functions.

Consider an async function f that returns an object of type Foo:

type Foo = { a: number }

const f = async () => ({ a: 1 })

I want the returned object to strictly be of Foo type, how do I enforce this?

1) Hard code the return type

const f = async (): Promise<Foo> => ({ a: 1 })

The problem here is that I must write Promise<Foo> instead of just Foo, but the Promise part can be inferred just fine. In real world these generics can be arbitrarily complex.

2) Declare a variable

const f = async () => {
  const result: Foo = { a: 1 }

  return result
}

This is better than retyping generics but still pretty verbose.

3) Use a helper function

const safeAssert = <T>(value: T): T => value

const f = async () => safeAssert<Foo>({ a: 1 })

This is ok but still requires writing/importing helper functions.

as is not an option here because it's unsafe, satisfies is neither because it doesn't name the type. Naming a type can be important for documentation purposes, this proposal allows to do this with less boilerplate.

const f = async () => ({ a: 1}) as! Foo // f: () => Promise<Foo>

💻 Use Cases

As described above, this feature can be used to safely cast expressions to specified types. A practical example when it can be useful is naming a type in an expression. Currently this is only covered by custom helper functions such as <T>(value: T): T => value.

RyanCavanaugh commented 11 months ago

This was discussed a bit at #47920. Your example is missing what I'd consider the most straightforward solution:

const f: () => Promise<Foo> = async () => ({ a: 1 })
bgenia commented 11 months ago

This was discussed a bit at #47920. Your example is missing what I'd consider the most straightforward solution:

const f: () => Promise<Foo> = async () => ({ a: 1 })

This has the same problem as my 1st example, you have to retype generics

Here's a more complex example to show why it's inconvenient:

type Foo<T> = { foo: T }
type Bar<T> = { bar: T }

declare function makeFoo<T>(value: T): Foo<T>
declare function makeBar<T>(value: T): Bar<T>

type Value = { value: number }

const f = async () => makeFoo(makeBar({ value: 1 }))

Now I have to type 3 generics:

const f: () => Promise<Foo<Bar<Value>>> = async () => makeFoo(makeBar({ value: 1 }))
RyanCavanaugh commented 11 months ago

you have to retype generics

This only applies to Promise, though. In all other cases, you're writing the full type again.

bgenia commented 11 months ago

you have to retype generics

This only applies to Promise, though. In all other cases, you're writing the full type again.

I used promises because it's the most obvious example, but you can have all kinds of functions that take your value and wrap it in different generics. I updated my example to demonstrate this.

Josh-Cena commented 11 months ago

The most painful thing this would remedy is cases such as #9998, which forces you to do true as boolean. I believe there are "best practices" these days that force you to do x satisfies T as T—based on feature requests we've closed in ts-eslint.

raythurnvoid commented 9 months ago

We would find this feature very useful since by using playwright, we create mocks for our application and we are using satisfies to ensure that the mock is correct for the given type. however we also have to use as to provide better intellisense in vscode for the given mock resulting in many satisfies T as T where T may be a very long string since we use types generated by grpc.

saltman424 commented 6 months ago

I was about to create a feature request, but I think this is asking for the same thing. Basically, I just want something like X satisfies as T to be syntactic sugar for X satisfies T as T. As mentioned by @Josh-Cena and @raythurnevoid, this seems to be a common and sometimes verbose pattern. Just some simple syntactic sugar to remove the duplication of T would be fantastic.

Also, just to link it, this feature would pretty much address: https://github.com/microsoft/TypeScript/issues/13626