Skip to content

Partial type assertionsΒ #53061

Closed
Closed

Description

Suggestion

πŸ” Search Terms

partial, procedural, type assertion, inference, as syntax, annotation, narrowing

βœ… Viability Checklist

My suggestion meets these guidelines:

  • 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 TypeScript's Design Goals.

⭐ Suggestion

Make it possible for type arguments to be inferred in a type assertion.

πŸ“ƒ Motivating Examples

We don't need to know the type parameters of nums

const nums = getNums() as Tuple<3, infer[0]> // [number, number, number];

// or
const nums = getNums() as ToTuple<3, infer>
type ToTuple<N extends number, T extends unknown> = Tuple<N, T[0]>

const getNums = () => [1, 2, 3];

type Tuple<L extends number, T = unknown, R extends unknown[] = []> =
    number extends L ? T[] : L extends R["length"] ? R : Tuple<L, T, [T, ...R]>;

We don't need to know the type constructor of numSet

const numSet = getNumSet() as Replace<infer, [number]> // Set<number>

const getNumSet = () => new Set<number | string>([3]);

import { Replace } from 'free-types' // nevermind that

We may only care about one part of the type

const data = getData() as ToData<infer> // [Date, ...(boolean | number)[]];

type ToData<T extends unknown[]> = [Date, ...Exclude<T[0], Date>[]]

const getData = () => [new Date('5/12/2020'), true, 50];

We sort of get generics in type annotations

const cookbook: ValidCookBook<infer> = {
    ingredients: {
        "tomato": { vegetal: true },
        "cheese": { vegetal: false },
        "lettuce": { vegetal: true },
    },

    recipes: {
        "pizza": ["tomato", "chees#@e"],
        //                  ~~~~~~~~~~ Did you mean "cheese"?
        "salad": ["tomato", "lettuce"]
    }
} // would be great if it could work with `satisfies` as well

type ValidCookBook<T extends CookBook> = {
    ingredients: T['ingredients'],
    recipes: { [K in keyof T['recipes']]: (keyof T['ingredients'])[] }
}

type CookBook = {
    ingredients: {[key: string]: {vegetal: boolean}},
    recipes: {[key: string]: readonly string[]}
}

πŸ’» Use Cases

Being less specific when asserting types

I often feel the need to narrow a type that was inferred, but I don't want to assert the type because it would require to be very specific about it, and satisfies is not an option (typically getNums() satisfies Tuple<3> is an error).

Being specific hurts maintainability and can also be a pain when the type is complex. For examples you may need to bring into scope types that you don't use elsewhere, or which are not even exported.

This can also affect readability in situations where creating the necessary temporary variables on which to use typeof adds noise and breaks the flow of reading. I guess it affects functional programming more where it is common to combine stuff in a tacit style.

A workaround is to use a wrapper function

const nums = toTuple(3, getNums()) // [number, number, number]
const toTuple = <N extends number, T extends unknown[]>(n: N, x: T) => x as Tuple<N, T[0]>;
const numSet = toNumber(getNumSet()) // number[]
const toNumber = <T>(x: T) => x as Replace<T, [number]>

This is not great, because I am used to functions having a runtime behaviour. toTuple even has an additional parameter to get around the lack of partial generic inference, which could give the impression that arrays longer than 3 are trimmed for example, or that the function throws if the tuple is not of length 3, which it does not.

Being able to use generics when checking variable declarations

I see a lot of type assertions/annotations or satisfies expressions in variable declarations to check that a value is well formed where it's defined, however concrete types are not expressive enough to validate that some pieces of types are related.

As a workaround, in addition to using a wrapper function, one can use a custom type-level assertion:

const cookbook = {
    ingredients: {
        "tomato": { vegetal: true },
        "cheese": { vegetal: false },
        "lettuce": { vegetal: true },
    },

    recipes: {
        "pizza": ["tomato", "chees#@e"],
        "salad": ["tomato", "lettuce"]
    }
} as const

{type Test = ValidateCookBook<typeof cookbook>}
//                            ~~~~~~~~~~~~~~~ Did you mean "cheese"?

type ValidateCookBook <T extends CookBook & IsValidCookBook<T>> = never;

type IsValidCookBook<T extends CookBook> = {
    ingredients: T['ingredients'],
    recipes: { [K in keyof T['recipes']]: readonly (keyof T['ingredients'])[] }
}

type CookBook = {
    ingredients: {[key: string]: {vegetal: boolean}},
    recipes: {[key: string]: readonly string[]}
}

That's one more option which at least does not involve runtime code, but the dev experience is a little worse and the downside is that the code that checks the value well-formedness is disconnected from its declaration. It feels like a hack or code that should belong to a test suite.


Code samples edited to reflect conversation with fatcerberus

Activity

  1. RyanCavanaugh commented on Mar 2, 2023

    @RyanCavanaugh
    Member

    There's definitely a duplicate of this, but unsurprisingly I can't find it

  2. fatcerberus commented on Mar 2, 2023

    @fatcerberus

    I'm not even really sure what the ask is here, tbh

  3. fatcerberus commented on Mar 2, 2023

    @fatcerberus

    There's definitely a duplicate of this, but unsurprisingly I can't find it

    Having re-read the OP a few times, I'm still not 100% clear on what it's asking for (inferring type assertions? wut) but I think it might be a dupe of #33480

  4. RyanCavanaugh commented on Mar 2, 2023

    @RyanCavanaugh
    Member

    Yeah, that's the one.

  5. geoffreytools commented on Mar 2, 2023

    @geoffreytools
    Author

    I had totally neglected the problem of contextual types and it had never occurred to me that they also worked with the as syntax.

    Bruce Pascoe (@fatcerberus), in a nutshell when you get types from functions, it is very common that what you get is wider than what you expect, for example a NonEmptyArray notoriously becomes an Array, you could also need a SortedArray type and it would also be widened to an Array, and that's supremely annoying because many algorithms require the narrow version of these types.

    The only way at present to deal with it in a systematic way is to go and carefully reimplement the whole Array interface for NonEmptyArray (fp-ts does that for example, thank you very much) and yes you could do that with SortedArray, you could do that with anything from any library, but you usually have better things to do, you want a type assertion, but the only thing you want to specify is "non-empty", "sorted", "numeric" etc. not the whole type.

  6. fatcerberus commented on Mar 2, 2023

    @fatcerberus

    I kind of figured that’s what you wanted to do but I think layering it on top of as type assertions is the wrong model - a type assertion is what you use when the compiler infers the wrong thing, it’s an override intended to overcome limitations in the type system, so then asking the compiler to infer parts of that override when you already know it’s not up to the task doesn’t make much sense conceptually.

  7. geoffreytools commented on Mar 2, 2023

    @geoffreytools
    Author

    Well, if it didn't make sense conceptually it wouldn't be everywhere on StackOverflow: people do use function wrappers to do exactly that and the OP always complains about having to define a runtime artefact in order to assert a type before reluctantly accepting the answer. It's just the way it is.

    I understand your reasoning about the inferred type being "wrong" and that, basically, "garbage in garbage out", but it's not "wrong", it's too wide, it's less usable. You would simply need to intersect it with something else (apart from functions I suppose)

    I do get that there is a circularity issue anyway so there is no point arguing.

  8. fatcerberus commented on Mar 2, 2023

    @fatcerberus

    No worries; this isn’t really criticism of your idea per se, more that I think the talk of type assertions confuses the real issue.

    What I guess I’m getting at is, if the compiler were hypothetically smart enough to be able to infer the narrower type where one was needed, you wouldn’t need to assert a type at all. So to bring up type assertions in this context muddies the waters, IMO. #33480 gets it right I think, that this would make more sense as part of a type annotation.

  9. RyanCavanaugh commented on Mar 2, 2023

    @RyanCavanaugh
    Member

    I guess I find this example pretty compelling and not impossible to reason about:

    type Box<T> = {
      a: T;
      b: T;
    };
    // Want error like on the 'checkBox' call, but otherwise leave this code unchanged
    const m = { a: 42, b: "hello world" };
    
    // Workaround
    checkBox(m);
    declare function checkBox<T>(arg: Box<T>): void;

    but the exact mechanics are pretty fuzzy. Like you might have

    type Box<T, U> = {
      a: T;
      b: U;
    };

    and want to talk about matching Box<thing, the same thing>, whereas if you write Box<infer, infer> that'd presumably be making two floating type variables. You'd probably want to be able to write Box<infer T, infer T> but that's confusing too because the T doesn't "go anywhere" and the scoping rules would be kinda weird (e.g. can you refer to a not infer T?)

  10. geoffreytools commented on Mar 3, 2023

    @geoffreytools
    Author

    Bruce Pascoe (@fatcerberus), in hindsight I realise I did write nonsensical things and it misrepresented the feature I was asking for. It is a lot more humble.

    The getData example would be better written like so

    const data = getData() as ToData<infer>
    type ToData<T extends unknown[]> = [Date, ...Exclude<T[0], Date>[]]

    So we are not asking TS to narrow an array to a tuple, we are doing most of the work ourselves.

    However, TS would complain that the asserted type does not sufficiently overlap with the inferred type because [Date ...rest[]] has a required element at index 0.

    We should have to write getData() as unknown as ToData<infer>, and infer should have to magically ignore unknown and refer to the original inferred type for it to be useful.

    The getNums example was also poorly written, it should be:

    const nums = getNums() as Tuple<3, infer[0]> // [number, number, number];

    or we could define a ToTuple wrapper like so

    type ToTuple<N extends number, T extends unknown[]> = Tuple<N, T[0]>
    const nums = getNums() as ToTuple<3, infer> 

    So we are not asking TS to guess that infer should be (typeof nums)[0], we are doing most of the work.

    I think this one would pass because the type checker would be fooled by the generic N: as per the implementation of Tuple, Tuple<number, T> is T[]. It certainly passes in the workaround implementation I provided.

    I apologise for the confusion it must have caused (Great, I'm sounding like ChatGPT now)

  11. geoffreytools commented on Mar 3, 2023

    @geoffreytools
    Author

    Ryan Cavanaugh (@RyanCavanaugh), I am not sure about what is meant here. I don't see anything constraining m

    // Want error like on the 'checkBox' call, but otherwise leave this code unchanged
    const m = { a: 42, b: "hello world" };

    Concerning Box<T, U>, I would solve it like so

    type Box<T, U> = {
      a: T;
      b: U;
    };
    
    type SameBox<T> = Box<T, T>;
    
    const m: SameBox<infer> = { a: 42, b: "hello world" }
    //                                 ~
    // workaround
    const m = sameBox({ a: 42, b: "hello world" })
    //                         ~
    const sameBox = <T>(x: SameBox<T>) => x as SameBox<T>;
    //                                      -------------
    // living `as` to match the behaviour of type assertion
    // but if `satisfies` could benefit from the same expressiveness I would remove it

    I would expect this mechanics to have limitations. We are already limited in the things we can do with utility types when the argument is generic (typically conditional types). If the same kind of restrictions apply with type assertions it should not surprise users.

    If the feature is limited to accessing typeof m as a type parameter in a utility type while asserting the type of m, it would make type assertions a lot more expressive, even with all kinds of limitations.

  12. typescript-bot commented on Mar 5, 2023

    @typescript-bot
    Contributor

    This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  13. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions