Repository navigation
Partial type assertionsΒ #53061
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 thatWe 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
RyanCavanaugh commentedon Mar 2, 2023
There's definitely a duplicate of this, but unsurprisingly I can't find it
fatcerberus commentedon Mar 2, 2023
I'm not even really sure what the ask is here, tbh
fatcerberus commentedon Mar 2, 2023
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
RyanCavanaugh commentedon Mar 2, 2023
Yeah, that's the one.
geoffreytools commentedon Mar 2, 2023
I had totally neglected the problem of contextual types and it had never occurred to me that they also worked with the
assyntax.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
NonEmptyArraynotoriously becomes anArray, you could also need aSortedArraytype and it would also be widened to anArray, 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
Arrayinterface forNonEmptyArray(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.fatcerberus commentedon Mar 2, 2023
I kind of figured thatβs what you wanted to do but I think layering it on top of
astype 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.geoffreytools commentedon Mar 2, 2023
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.
fatcerberus commentedon Mar 2, 2023
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.
RyanCavanaugh commentedon Mar 2, 2023
I guess I find this example pretty compelling and not impossible to reason about:
but the exact mechanics are pretty fuzzy. Like you might have
and want to talk about matching
Box<thing, the same thing>, whereas if you writeBox<infer, infer>that'd presumably be making two floating type variables. You'd probably want to be able to writeBox<infer T, infer T>but that's confusing too because theTdoesn't "go anywhere" and the scoping rules would be kinda weird (e.g. can you refer to a notinferT?)geoffreytools commentedon Mar 3, 2023
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
getDataexample would be better written like soSo 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>, andinfershould have to magically ignoreunknownand refer to the original inferred type for it to be useful.The
getNumsexample was also poorly written, it should be:or we could define a
ToTuplewrapper like soSo we are not asking TS to guess that
infershould 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 ofTuple,Tuple<number, T>isT[]. 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)
geoffreytools commentedon Mar 3, 2023
Ryan Cavanaugh (@RyanCavanaugh), I am not sure about what is meant here. I don't see anything constraining
mConcerning
Box<T, U>, I would solve it like soI 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 mas a type parameter in a utility type while asserting the type ofm, it would make type assertions a lot more expressive, even with all kinds of limitations.typescript-bot commentedon Mar 5, 2023
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.