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

Mechanism for late inferred types in control flow analysis #60907

Open
6 tasks done
kolodny opened this issue Jan 2, 2025 · 2 comments
Open
6 tasks done

Mechanism for late inferred types in control flow analysis #60907

kolodny opened this issue Jan 2, 2025 · 2 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

@kolodny
Copy link

kolodny commented Jan 2, 2025

πŸ” Search Terms

"control flow analysis" with variations of array, widening, callbacks, etc

βœ… Viability Checklist

⭐ Suggestion

I have an issue where I want to capture a type defined in a scope that is inaccessible from my current scope. For example, I want to create a wrapper for some OpenAI function.

Playground

import 'openai'; // Just to get the types to load

type Magic = never;
// let expandingArray: Array<Magic> = []

let expandingArray = []

type Client = InstanceType<typeof import('openai').OpenAI>

const openai = {} as Client;

type Options = {
    onMessageResponse: (message: typeof expandingArray[number]) => void
}

const doStuff = (options: Options) => {
    openai.chat.completions.create({
        model: 'chatgpt-4o-latest',
        messages: []
    }).then(response => {
        const {role, ...rest} = response.choices[0].message

        // Here's where I finally resolve how I want the shape to look like
        expandingArray.push(
            {...rest, type: 'my-custom-type' as const}
        )
        type Good = typeof expandingArray[number]; // Good is typed correctly but local to this scope

        options.onMessageResponse(expandingArray[0]) // This should be type-safe and not just never
    })
}

type Bad = typeof expandingArray;

As you can see, the goal is to have an auto-widening type based on usage later on (within the same file). This somewhat already works with control flow analysis for arrays, as you can see in the type Good = ... type above.

Realistically, when this situation occurs, application authors will type onMessageResponse as (message: any) => void, which is unfortunate because all the type information is there and just needs a way to be bubbled up to the higher scope.

It would be nice if there was an escape hatch to tell the type system that I care about the typeof a value declared somewhere inaccessible to the current type scope within the file. Obviously, this wouldn't actually use an untyped array with some annotation on the type; this would probably be a special new named type like LateInferredType or something similar, e.g.:

type Message = LateInferredType;

type Options = {
    onMessageResponse: (message: Message) => void
}

/// ...
    .then(response => {
        const {role, ...rest} = response.choices[0].message

        // Here's where I finally resolve how I want the shape to look like
        const message = {...rest, type: 'my-custom-type' as const}

        options.onMessageResponse(message as Message) // Perhaps casting signals to the type system to recognize it
    })

Or perhaps there's a non-breaking way of declaring this syntax that I'm not thinking of.

πŸ“ƒ Motivating Example

This allows smarter inference without needing to chase down every derived property. Sometime the specific types aren't even exported by the upstream libraries and there's no good way to mention them without recreating a large complex type which is fragile and error prone.

πŸ’» Use Cases

  1. What do you want to use this for?
    This is useful for writing type safe code where you want to expose a complex type that doesn't have a concrete definition and is more tied to the specific shape of a single step in a complex application
  2. What shortcomings exist with current approaches?
    There's no way to signal later in a program or without a callback that a type should be widened, but sometimes that exact functionality is needed
  3. What workarounds are you using in the meantime?
    None
@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 Jan 2, 2025
@itsUndefined
Copy link

Why not write your results processing callback into a new function? This way you can get the inferred types:

const processResponse = (response: Response) => {
    const {role, ...rest} = response.choices[0].message
    return {...rest, type: 'my-custom-type' as const}
}

type Options = {
    onMessageResponse: (message: ReturnType<typeof processResponse>) => void
}

/// ...
    .then(response => {
        options.onMessageResponse(processResponse(response))
    })

@kolodny
Copy link
Author

kolodny commented Jan 3, 2025

Why not write your results processing callback into a new function? This way you can get the inferred types:

const processResponse = (response: Response) => {
    const {role, ...rest} = response.choices[0].message
    return {...rest, type: 'my-custom-type' as const}
}

type Options = {
    onMessageResponse: (message: ReturnType<typeof processResponse>) => void
}

/// ...
    .then(response => {
        options.onMessageResponse(processResponse(response))
    })

The example is contrived, in the case above, you're assuming that there's a Response type I can attach to, what if that type isn't exported by the library. I think this highlights the issue of needing to chase down all the types when I want to do any refactoring or exposing a part of the logic

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

3 participants