Comments (22)
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
from typescript.
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:
For the most part where people want exact types, we'd prefer to fix that by making EPC smarter.
This is me wanting a way to make EPC smarter! The EPC I get with satisfies
is 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.
from typescript.
You might want to take a look into the recent improvement from #55811
type Foo = { a: number };
const foo = <T extends Foo>(x: { [K in keyof T & keyof Foo]: T[K] }) => x;
foo({
a: 1,
// Object literal may only specify known properties, and 'wrong' does not exist in type '{ a: 1; }'.(2353)
wrong: 2,
});
from typescript.
The basic confusion here comes down to the fact that extends
and satisfies
both mean "assignable to", the only difference between them is that extends
applies to types and satisfies
to values. EPC is a check done on values; T extends U
as a generic constraint enforces that the type T
is assignable to the type U
; EPC doesn't apply because there's no value involved at the point that check happens. T satisfies U
is 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 than Foo
violates the constraint, but we still want to infer the types for each property.
Footnotes
-
This syntax is itself instructive: the
const
keyword 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. ↩
from typescript.
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 satisfies
would 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 @Andarist !
from typescript.
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.
from typescript.
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?
type OptionalKeys<T extends Record<string, unknown>> = {
// eslint-disable-next-line
[P in keyof T]: {} extends Pick<T, P> ? P : never;
}[keyof T];
type RequiredKeys<T extends Record<string, unknown>> = {
// eslint-disable-next-line
[P in keyof T]: {} extends Pick<T, P> ? never : P;
}[keyof T];
type Satisfies<T, Base> =
// recur if both the generic type and its base are records
T extends Record<string, unknown>
? Base extends Record<string, unknown>
// this check is to make sure i don't intersect with {}, allowing any keys
? (keyof T & RequiredKeys<Base> extends never
? unknown
: {
[K in keyof T & RequiredKeys<Base>]: Satisfies<
T[K],
Base[K]
>;
}) &
// this check is to make sure i don't intersect with {}, allowing any keys
(keyof T & OptionalKeys<Base> extends never
? unknown
: {
[K in keyof T & OptionalKeys<Base>]?: Satisfies<
T[K],
Base[K]
>;
})
// if the generic type is a record but the base type isn't, something has gone wrong
: never
: T extends (infer TE)[]
? Base extends (infer BE)[]
? Satisfies<TE, BE>[]
// if the generic type is an array but the base type isn't, something has gone wrong
: never
// the type is a scalar so no need to recur
: T;
which leads to desired behaviour:
type Foo = { a: { b?: number }[] };
const foo = <T extends Foo>(x: Satisfies<T, Foo>) => x;
foo({ a: [{}] }); // typechecks
foo({ a: [{ b: 3 }] }); // typechecks
foo({ x: 2 }); // error
foo({ a: [{}], x: 2 }); // error
foo({ a: [{ b: 2, x: 3 }] }); // error
foo({ a: [{ x: 3 }] }); // error
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.
from typescript.
@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:
const result = await db.create("post", {
data: { title: "hello", content: "it me", authorId: "123", wrong: 2 },
returning: ["authorId"],
});
db
is 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 use satisfies
at the call site:
const result = await db.create("post", {
data: { title: "hello", content: "it me", authorId: "123", wrong: 2 },
returning: ["authorId"],
} satisfies CreateParams<Models, M>);
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!
from typescript.
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?
from typescript.
@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.
from typescript.
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. satisfies
and extends
do the same thing - check that the right operand is a subtype of the left operand.
from typescript.
There's already const
type 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 Foo
won't complain against excess properties on T
because T
will 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 stops
interface Foo {x: string}
interface Bar extends Foo {y: string}
const bar: Bar = {x: "",y: ""}
const foo: Foo = Bar; // <-- nothing can ever stop this
That's allowed because bar
will remember y
, even though foo
will not. Only const 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.
from typescript.
Like, let's say this operator existed
type Foo = { a: number };
const foo = <T satisfies Foo>(x: T) => x;
foo({ a: 1, wrong: 2 })
What would happen?
During inference we'd see a candidate { a: number, wrong: number }
for T
, so would infer T
to 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:
type Foo = { a: number };
const p = { a: 1, wrong: 2 };
p satisfies Foo; // it does
satisfies
does not have special additional type behavior that extends
doesn't
from typescript.
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 satisfies
and 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):
type Foo = {a: number}
// Satisfies works differently given a literal vs a variable
const x = {a: 3, b: 4 } satisfies Foo // it doesn't
const y = {a: 3, b: 4} as const;
y satisfies Foo // it does
// --------------
// Calling a function works differently given a literal vs a variable
const f1 = (x: Foo) => x;
f1({a: 3, b: 4 }) // doesn't work
f1(y) // this works fine
// --------------
// However, calling a generic function works the same in both cases
const f2 = <const T extends Foo>(x: T) => x;
f2({ a: 3, b: 4 }) // works - and i want a way to stop it from working
f2(y) // also works - and **should continue to work**
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 satisfies
being different to extends
comes from the lack of symmetry w/r/t excess property checking. If satisfies
didn't do excess property checking I wouldn't have this expectation.
from typescript.
You can do this:
const f2 = <const T extends Foo>(x: NoInfer<T>) => x;
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 information
from typescript.
I completely understand that you're asking for a relation between types where more-specific property types are allowed, but not extra keys. But satisfies
isn't that relation. You're misattributing the cause of why you see the error in one context but not the other.
from typescript.
You're misattributing the cause of why you see the error in one context but not the other.
Ah ok, that is helpful, thanks. In that case, what is it that causes EPC to happen when doing something like this?
type Foo = { a: number };
const x = { a: 1, b: 2 } satisfies Foo;
Is it the const
assignment rather than the satisfies
that is triggering EPC in this case? Because const 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?
from typescript.
The cause is specifically that the object literal being assigned to x
is contextually typed by Foo
. The expression x satisfies T
roughly means "typecheck x
as if it's being assigned to something of type T
". So if x
is an object literal, you get EPC.
from typescript.
The expression x satisfies T roughly means "typecheck x as if it's being assigned to something of type T".
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:
const duplicateFoo = <T satisfies Foo>(x: T): T[] => [x, x]
...“type check the argument x
as 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 satisfies
keyword, it could be const foo = <U extendsWithEPC T> (x: U) => x
… but that feels needlessly wordy and confusing - and I would never expect EPC and extends
to go together. While my expectation for functionality that uses the satisfies
keyword is that it would have EPC.
For eg let’s say as well as
const x = { a: 1 } satisfies Foo
we had the (imaginary) syntax
const x = { a: 1 } extends Foo
I would not expect the second example to do EPC because that’s just not how extends
works: anything with a superset of Foo
’s properties should typecheck fine. But satisfies
does trigger EPC here, which is why it feels like something different to extends
. 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:
//given a type `Foo`
type Foo = { a: number };
// a function that takes anything that satisfies foo, with EPC, and retains its literal type
const fnGenericSatisfies = <T satisfies Foo>(x: T) => x;
// a function that takes exactly `Foo`, no extra properties allowed
const fnExact = (x: Exact<T>) => x;
// a function that extends an `Exact` version of `Foo` - but what does it
// even mean to extend an `Exact` type?
// either it's meaningless/not possible, or the type is no longer `Exact`,
// or it's an `Exact` type with extra fields.... this imo is one of the
// problems with the idea of exact types. it's not clear what to do in
// this situation. so going to ignore this third case for the remaining
// examples
const fnGenericExact = <T extends Exact<Foo>>(x: T) = x;
// literal case with valid argument
fnGenericSatisfies({ a: 1 }) // no error => { a: 1 }
fnExact({ a: 1 }) // no error => Foo / { a: number }
// literal case with argument that has extra properties
fnGenericSatisfies({ a: 1, b: 2 }) // type error due to EPC
fnExact({ a: 1, b: 2 }) // type error due to "exactness" of type
// passing a variable
const x = { a: 1 };
fnGenericSatisfies(x) // no error => { a: number }
fnExact(x) // maybe this works, or maybe it's an error bc `typeof x` is not an `Exact` type ?
// passing a variable with const type
const x = { a: 1 } as const;
fnGenericSatisfies(x) // no error => { a: 1 }
fnExact(x) // maybe this works, or maybe it's an error bc `typeof x` is not an `Exact` type ?
// passing a variable with extra properties
const x = { a: 1, b: 2 };
fnGenericSatisfies(x) // no error => { a: 1, b: 2 }
fnExact(x) // type error because `typeof x` is not exactly `Foo`
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:
export type DisallowExtraKeys<Base, T extends Base> = {
[K in keyof T]: T[K];
} & {
[K in keyof T as K extends keyof Base ? never : K]: never;
};
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:
type Foo = { a: number };
const foo = <T extends Foo>(x: DisallowExtraKeys<Foo, T>) => x;
foo({ a: 1 }); // OK
foo({ a: 1, b: 2 }); // Error: Type 'number' is not assignable to type 'never'.(2322)
const x = { a: 1 };
const y = { a: 1, b: 2 };
foo(x); // OK
foo(y); // Error: Argument of type '{ a: number; b: number; }' is not assignable to parameter of type '{ a: number; }'. Object literal may only specify known properties, and 'b' does not exist in type '{ a: number; }'.(2345)
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 the satisfies
generic described in this feature request, cause a type error), and the errors can be a bit weird due to the never
types, 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.
from typescript.
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 const
keyword in generics. High likelihood I'll be using this terminology incorrectly, but maybe something like T narrows Foo
or narrow T extends Foo
would 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):
type Foo = {a:number}
const foo = <const T extends Foo, _EPC = unknown>(x: T & _EPC) => x.a
foo({a: 1}) // typechecks, but as `number`, not `1`
foo({a: 1, b: 2}) // type error
const x = {a: 1, b: 2}
foo(x) // typechecks
I have no idea why this does what it does.
from typescript.
Based on now understanding the distinction of satisfies
=> values and extends
=> types, would any of these options be more viable as a proposed feature request for this kind of behaviour?
const f1 = (x: Foo as const) => x
const f2 = (x: const Foo) => x
const f3 = (const x: Foo) => x
const f4 = (x: satisfies Foo) => x
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).
from typescript.
I commented the same here and the possibility of replacing satisfies
by Exact
here.
There is not a simple way of using a generic type with excess property checking (playground):
type User = {
name?: string
enabled?: boolean
}
const f1 = <const T extends User>(input: T): T => input
f1({ enabled: true }).enabled; // [success] "enabled" is `true`
f1({ enabled: true, surname: "" }).surname; // [error] no error
const f2 = <const T extends User, $Exact = unknown>(input: T & $Exact): T => input
f2({ enabled: true }).enabled; // [error] "enabled" is `boolean | undefined`
f2({ enabled: true, surname: "" }); // [success] property error
const f3 = <T extends User>(input: T): T => input
f3({ enabled: true } satisfies User).enabled; // [success] "enabled" is `true`
f3({ enabled: true, surname: "" } satisfies User); // [success] property error
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
type Exact<T, Shape> = T extends Shape ? ExactKeys<T, Shape> : never;
const f4 = <const T extends User>(input: Exact<T, User>): T => input
f4({ enabled: true }).enabled; // [success] "enabled" is `true`
f4({ enabled: true, surname: "" }); // [success] property error
You can use satisfies
but...
- You must add it in every part of the code.
- If you forget add it there will be no error.
Or you can try to emulate Exact
but...
- If you have recursive types is complex.
- The performance will decrease, especially if you use it in recursive types.
- The warning of excesive property affects to all types, so finding the error is complicated.
<T satisfies User>(input: T)
or <T extends Exact<User>>(input: T)
would solve all the problems.
from typescript.
Related Issues (20)
- Error: Debug Failure. False expression: Expected the specifier to be a default export HOT 1
- type assertions affect the type narrowing of the subsequent code HOT 6
- Missing interface definition for Highlight API HOT 1
- Improve new issue template selection - reorder and handle "this is crash" check better HOT 3
- Union type breaks type safety of dynamic object keys HOT 2
- Asserting changes type of original variable inline (the change reverts in subsequent lines) HOT 10
- "declare class" in a global is not visible in external files HOT 7
- Mapped type reported as incompatible when passed through another mapped type (5.4 regression) HOT 1
- Shorten error spans for missing key errors reported on object literals HOT 3
- Array prototype extensions fail circularly with `...args: T[]`, but not `args: T[]` HOT 6
- Inconsistent behaviour of es6 and esnext property initializers when using experimental property decorators HOT 3
- Spread operator doesn't remove readonly from object properties HOT 3
- Add option to throw error when `!` (NonNull expression) is used on a non-nullish expression HOT 2
- Inconsistent Type Narrowing with `never` Return Type Between Arrow and Traditional Functions HOT 2
- Class infers wrong generic parameter since e1874f3 HOT 2
- Promise returned from sync `dispose` method should not be awaited when disposing an asyc-disposable HOT 1
- Import ellision emit bug: usage of imports only in keys of interfaces does not result in those imports being elided as type-only
- `noUncheckedIndexedAccess` does not narrow properly when iterating with `for...in` HOT 4
- Omit private symbols from type HOT 6
- TS5055: Typescript includes files in "dist" folder for compilation when `rootDir` is not `./` but `./src` even after explicitly adding "dist" to `exclude` HOT 7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from typescript.