Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe type assertion operator #56235

Open
5 tasks done
bgenia opened this issue Oct 27, 2023 · 7 comments
Open
5 tasks done

Safe type assertion operator #56235

bgenia opened this issue Oct 27, 2023 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@bgenia
Copy link

bgenia commented Oct 27, 2023

πŸ” Search Terms

as, satisfies, type assertion, type casting

βœ… Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

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

  • Like as, it should cast the expression to the asserted type
  • Like satisfies, it should only allow assertion if the asserted type is wider than the expression type

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.

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

  return result
}

This is better than retyping generics but still pretty verbose.

  1. 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
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Oct 27, 2023
@bgenia
Copy link
Author

bgenia commented Oct 27, 2023

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
Copy link
Member

you have to retype generics

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

@bgenia
Copy link
Author

bgenia commented Oct 27, 2023

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
Copy link
Contributor

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.

@raythurnevoid
Copy link

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
Copy link

saltman424 commented Mar 28, 2024

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: #13626

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants