Repository navigation
Support satisfies as a constraint on a function's generic type parameter #58031
Description
🔍 Search Terms
"satsifies", "satisfies generic parameter".
i found two similar/related feature requests, but they felt different enough to warrant being a separate feature request:
✅ 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 isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
Currently, from what I can tell, is not possible to mimic the behaviour of satisfies on a function's generic type parameter - i.e. to define a function with a generic parameter that allows inference of a literal type that conforms to that generic parameter, while still benefitting from excess property checking. Because the only option for a generic type parameter is extends, there's no way (that I can see) to prevent extra keys in the inferred generic type.
📃 Motivating Example
A bare bones example of what this change would make possible:
type Foo = { a: number };
const foo = <T extends Foo>(x: T) => x;
foo({ a: 1, wrong: 2 }) // typechecks, but i don't want it to
foo({ a: 1, wrong: 2} satisfies Foo) // causes the type error i wantWhat I would really like to be able to write:
type Foo = { a: number };
const foo = <T satisfies Foo>(x: T) => x;
foo({ a: 1, wrong: 2 }) // causes the type error i want💻 Use Cases
- What do you want to use this for?
The reason I want this functionality is that I'm writing an ORM and would like to restrict the keys passed to the create function to only the keys present in the model. To be clear I do not need a runtime check and any extra keys actually in the model are fine. The reason I want this feature is to provide a good experience when writing code like this with literal types:
class DB<Models extends BaseModels> {
public async create<
M extends ModelName<Models>,
P extends CreateParams<Models, M>,
>(m: M, params: P): Promise<CreateResult<Models, M, P>> {
// ...omitted for brevity
}
// ...omitted for brevity
}
db.create("post", {
data: {
title: "hello",
content: "it me",
authorId: uuid.v4(),
// @ts-expect-error field does not exist
wrong: 2,
},
returning: ["id"],
// this causes a type error if `satisfies` is used, but does not cause a type error if it is not
} satisfies CreateParams<typeof db.models, "post">);- What shortcomings exist with current approaches?
It is only possible to use satisfies at the call site of a function, and must be used at every call site, rather than being specified as part of the function's generic type parameter.
- What workarounds are you using in the meantime?
I have not been able to find a workaround. If there is one, I would be very happy to hear it! If not I think the only option is to use satisfies at every call site.
Activity
Andarist commentedon Apr 2, 2024
You might want to take a look into the recent improvement from #55811
rsslldnphy commentedon Apr 2, 2024
nice!! thank you, that gives me the behaviour that i need. it's pretty complex to represent so a simpler way to do it via
satisfieswould still be much appreciated.i haven't got it fully working, am still getting some weird errors with other (optional) keys in the constrained type seeming to lose some type information, but this is a big step forward, and once i've managed to put together a minimal reproduction of the issue i'm now facing i'll ask on stack overflow. thanks again Mateusz Burzyński (@Andarist) !
Andarist commentedon Apr 2, 2024
It has currently some limitations (like #56910 ). If you are hitting some other thing - please let me know. If you provide a repro I could take a look at it.
rsslldnphy commentedon Apr 2, 2024
ok i think i've got it working with a fairly monstrous type. forgive the messiness as i'm pretty new to (relatively) more advanced typescript stuff. if there are ways to simplify this or any obvious problems i'd really appreciate a pointer!
i found i had to treat required and optional properties separately, but maybe there's an easier way to do this?
which leads to desired behaviour:
EDIT - ignore this, i thought it was working but it isn't (in my codebase specifically), in a way that i haven't been able to minimally reproduce yet.
fatcerberus commentedon Apr 2, 2024
I'm a little confused about the use case honestly - if you don't want subtypes, why is the function generic? Also note that EPC is more of a lint check than a proper type rule, and it's very easy to sneak extra properties in simply by, e.g., assigning the object literal to a variable first. See #12936
rsslldnphy commentedon Apr 2, 2024
Bruce Pascoe (@fatcerberus) the specific use case i want this for is for an ORM that allows users to specify the types of their models such that queries return strongly typed results. the aim is for users to get useful type hints when writing queries as literal objects. For example, in the below code:
dbis a generically typed representation of the user's database schema, which is aware of what models it contains and what field each model has. so typescript is able to know that "title", "content", and "authorId" are all valid fields on the "post" model. "wrong" however is not. what i'm trying to get to is a situation where typescript will show the "wrong" property as a type error, to let the user know they've done something wrong. this is exactly the behaviour you get if you usesatisfiesat the call site:but ideally i'd like to build this check into the function itself. it's fine to sneak extra properties in, they will be safely ignored. the purpose of the typing here is only to help the user of the library write correct code when writing literal queries.
there's clearly something very odd going on with the types in my project though, they're behaving weirdly in a way i've yet to figure out how to replicate in a ts playground
EDIT - so yes, as you say, the intention is for it to be more of a lint check than a proper type rule. if there's an alternative / better way to achieve this i'd really appreciate a pointer!
jcalz commentedon Apr 2, 2024
This is essentially a duplicate of #12936, since we're not looking for all of
satisfies's behavior, just excess property checking. Until and unless TS supports this, it's much easier to write code that doesn't care about excess properties than it is to write code that forces TypeScript to care about them. Especially because it's always going to be trivial to work around such code, by just someone widening the value before passing it.I don't understand why you'd need this if you just want the user to write correct code, though. IntelliSense should prompt for the known keys, and not for unknown ones. Why do you need an error for unknown keys?
rsslldnphy commentedon Apr 2, 2024
Joe Calzaretta (@jcalz) i'm not sure this is a duplicate, as i do want all of
satisfies' behaviour (at least as i understand it): excess property checking AND the preservation of literal types - both are necessary.if i didn't need the preservation of literal types, i wouldn't have to use a generic, and excess property checking would work fine. if there's a better/simpler way to achieve both those things i'd be v keen to take a different approach!
you're right that intellisense will prompt for the known keys and not unknown ones, but if the user types or copy/pastes an incorrect one there will then be no feedback to them that they've made a mistake. basically the reason i want an error for unknown keys is the same reason i find them useful when not using a generic - they let me know when i've made a mistake!
i feel like this is uncontroversial in the case of normal excess property checking, so the fact it seems controversial in this case is suggesting to me that i must be fundamentally misunderstanding or missing something, but i don't know what.
RyanCavanaugh commentedon Apr 2, 2024
I don't understand the distinction either. Either you're subtyping or you're not. If you don't want extra-keys subtyping, the feature you need is exact types.
satisfiesandextendsdo the same thing - check that the right operand is a subtype of the left operand.jcalz commentedon Apr 2, 2024
There's already
consttype parameters for literal types.Excess property checking is only supposed to be "normal" in cases where TS promptly forgets the excess property. The mistake isn't "there's an extra property" but "there's a property TS can't possibly remember". So
T extends Foowon't complain against excess properties onTbecauseTwill hold the information. Excess property checking is similar to having exact types, and there's overlap, but it's not the same feature. Even with "normal" excess property checking, nothing stopsThat's allowed because
barwill remembery, even thoughfoowill not. Onlyconst foo: Foo = {x: "", y: ""}is an error.Without #12936, there's no way to say universally that excess properties are "a mistake" by their mere presence.
With the current state of TS, it's going to be much easier to get out of the mindset that excess properties are "a mistake" and get into the mindset that it's just how structural typing works. That lets you write code that doesn't explode in the face of excess properties and move on, instead of struggling to represent a concept TS just doesn't know how to deal with.
RyanCavanaugh commentedon Apr 2, 2024
Like, let's say this operator existed
What would happen?
During inference we'd see a candidate
{ a: number, wrong: number }forT, so would inferTto be that type. Then we'd check that{ a: number, wrong: number }is a subtype of{ a: number }. It is, because extra keys are allowed in subtyping. So the call succeeds.It's the same as if you had written this:
satisfiesdoes not have special additional type behavior thatextendsdoesn'trsslldnphy commentedon Apr 2, 2024
I appreciate your time with this - going to make one last attempt to try to articulate where my confusion is. Yes, once something is assigned to a variable, all bets are off. However, both
satisfiesand calling a normal non-generic function behave differently depending on whether they are given a variable or a literal value. While with a generic there is only one behaviour.Extending your example a bit (here's a playground link):
I guess my feature request comes from the (perhaps misplaced) intuition that it should be possible for there to be symmetry here, and my understanding of
satisfiesbeing different toextendscomes from the lack of symmetry w/r/t excess property checking. Ifsatisfiesdidn't do excess property checking I wouldn't have this expectation.RyanCavanaugh commentedon Apr 2, 2024
You can do this:
Your objection will be "But I still want to use the more-specific type information of
T", and (without exact types) extra keys are part of that more-specific type informationrsslldnphy commentedon Apr 2, 2024
Ok I'm giving up now but just to say one last time that I do not want exact types. I want a way to tell Typescript to be smarter with excess property checking.
Which I think aligns pretty well with what you wrote on the exact types issue:
This is me wanting a way to make EPC smarter! The EPC I get with
satisfiesis very helpful, and I'd like the opportunity to use it in more contexts - that's it.As I say, I appreciate your time. I'm frustrated that I didn't come to an understanding as to why this isn't a good idea, but maybe as I learn more I will.
RyanCavanaugh commentedon Apr 2, 2024
I completely understand that you're asking for a relation between types where more-specific property types are allowed, but not extra keys. But
satisfiesisn't that relation. You're misattributing the cause of why you see the error in one context but not the other.rsslldnphy commentedon Apr 2, 2024
Ah ok, that is helpful, thanks. In that case, what is it that causes EPC to happen when doing something like this?
Is it the
constassignment rather than thesatisfiesthat is triggering EPC in this case? Becauseconst x: Foo = { a: 1, b: 2}also triggers EPC and would also cause a type error? Or is the cause of the error in this context something else?fatcerberus commentedon Apr 3, 2024
The cause is specifically that the object literal being assigned to
xis contextually typed byFoo. The expressionx satisfies Troughly means "typecheckxas if it's being assigned to something of typeT". So ifxis an object literal, you get EPC.rsslldnphy commentedon Apr 3, 2024
Right, this exactly matches my understanding, so I guess I’m still confused as to what I’m missing.
The proposal for the new syntax is basically, given a simple example:
...“type check the argument
xas if it’s being assigned to something of type Foo, and assign its actual/literal type to the type variable T”. That feels like pretty much the same thing in a different context - but I don’t understand how typescript works under the hood so maybe there’s a crucial difference?Ofc it wouldn’t have to use the
satisfieskeyword, it could beconst foo = <U extendsWithEPC T> (x: U) => x… but that feels needlessly wordy and confusing - and I would never expect EPC andextendsto go together. While my expectation for functionality that uses thesatisfieskeyword is that it would have EPC.For eg let’s say as well as
we had the (imaginary) syntax
I would not expect the second example to do EPC because that’s just not how
extendsworks: anything with a superset ofFoo’s properties should typecheck fine. Butsatisfiesdoes trigger EPC here, which is why it feels like something different toextends. What am I missing? Why are they the same thing? Why do they have different names?And, apologies for flogging the dead horse of "why this proposal is not the same as exact types": I've read the exact types issue several times now, and I cannot see how it and this proposal are the same, but maybe I'm completely misunderstanding exact types. It's probably easiest to explain what I see as the differences by way of examples:
Do I have some fundamental misunderstanding of what the exact types proposal is suggesting?
In any case - and for anyone who comes across this issue with a similar problem, I've now got roughly what I need working using this utility type:
It even seems to work with nested objects but I honestly have no idea why or how, because there's no recursion in the type itself.
This is its behaviour:
it's not quite what I want, it's stricter with variables in a way that I don't need and does feel closer to exact types (the
foo(y)example would not, with thesatisfiesgeneric described in this feature request, cause a type error), and the errors can be a bit weird due to thenevertypes, but so far it is doing the job. I wish I understood why it's working for nested objects, but I at least have a bunch of tests around it so I'll know if I accidentally break it.fatcerberus commentedon Apr 3, 2024
The basic confusion here comes down to the fact that
extendsandsatisfiesboth mean "assignable to", the only difference between them is thatextendsapplies to types andsatisfiesto values. EPC is a check done on values;T extends Uas a generic constraint enforces that the typeTis assignable to the typeU; EPC doesn't apply because there's no value involved at the point that check happens.T satisfies Uis a category error because the LHS is a type, not a value.FWIW, I also consider your feature request to be a de facto request for exact types since the behavior you want is precisely
const T extends Exact<Foo>1. i.e. anything with more keys thanFooviolates the constraint, but we still want to infer the types for each property.Footnotes
This syntax is itself instructive: the
constkeyword comes before the type parameter because it's not actually part of the constraint - it's a modifier that affects inference/typechecking for the value(s) it applies to. If there's a path forward here that doesn't involve exact types, I'd imagine this pattern is what would have to be emulated. ↩rsslldnphy commentedon Apr 3, 2024
Thank you!! That's a really clear explanation and the distinction 100% makes sense. And yes I was wondering about the relationship between what I was trying to ask for and the
constkeyword in generics. High likelihood I'll be using this terminology incorrectly, but maybe something likeT narrows Fooornarrow T extends Foowould get across the intent?Thanks for your patience in explaining this - I now actually understand the objections to this feature request (and fortunately I have a workaround that lets me do what I need to without it). Would love to see something like this added in a way that doesn't require full on exact types, but if this isn't a common need I would definitely understand hesitance to add more syntax to the language.
FWIW a helpful commenter on Reddit shared this approach to triggering EPC for generic types that nearly gets me exactly what I want (except it looks like literal types aren't preserved):
I have no idea why this does what it does.
rsslldnphy commentedon Apr 5, 2024
Based on now understanding the distinction of
satisfies=> values andextends=> types, would any of these options be more viable as a proposed feature request for this kind of behaviour?I hope the above proposals make a bit more sense, and make the intention of the functionality a bit clearer.
(Also, happy to stop posting on this issue if it is going nowhere and is unhelpful, don't want to take up your time needlessly).
juanrgm commentedon Apr 5, 2024
I commented the same here and the possibility of replacing
satisfiesbyExacthere.There is not a simple way of using a generic type with excess property checking (playground):
You can use
satisfiesbut...Or you can try to emulate
Exactbut...<T satisfies User>(input: T)or<T extends Exact<User>>(input: T)would solve all the problems.typescript-bot commentedon Apr 8, 2024
This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
xsanda commentedon Jun 30, 2024
I can add to this with a sample use case:
In my test files, I provide a subset of the translations file, so that those translations can be used in the test. I do this with
n.b. PartialTranslationFile is a recursively Partial version of
typeof enUS, givenimport type enUS from './en-us.json'.I would like to be able to omit the
satisfies PartialTranslationFile, by including it in the function signature instead, but currently if I were to remove it, I wouldn’t get warned for unknown translation keys.I can’t just let
makeTranslationstake and return the type PartialTranslationsFile itself, because thentranslations.closewould be typed asstring | undefinedrather than juststring, sotoUpperCase()would not work. Additionally, autocomplete would suggest all other properties of translation, which are not relevant for this test.Lordfirespeed commentedon Sep 8, 2024
Adding my own use-case here, too:
The type error occurs because
keyis typed asT, which may actually be an extension of the string literal, as opposed to the string literals themselves, the possible literal types ofpascalCase(key)cannot be inferred.To that end, what I want is
whereby
T's type is narrowed to"packageManager" | "runtime", sopascalCase(key)can be inferred correctly