-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Make satisfies
configurable to allow excess properties in nested values
#52999
Comments
For me and probably many others its important that interface User {
name: string
}
const john = {
name: 'John',
email: 'john@gmail.com',
}
// compile time check if john is assignable to User
;(x: typeof john): User => x
// or in your case:
type Config = {
key1: string
} // & { <A_COMPLEX_TYPE> }
const config = {
key1: "config1",
// keys with values of A_COMPLEX_TYPE
keyNotInConfig: "foo"
}
// compile time check if config is assignable to Config
;(x: typeof config): Config => x PS: If you want to check for equivalence you can do this: interface User {
name: string
}
const john = {
name: 'John',
email: 'john@gmail.com',
}
// compile time check if john is equivalent to User
;(x: typeof john): User => x
;(x: User): typeof john => x |
This is intentional. See #47920. They also mention a workaround if this behaviour is not desired for you: write |
satisfies
shouldn't complain about unknown propertiessatisfies
configurable to allow excess properties
@MartinJohns thank you, I missed that this was intentional behaviour first time I read that. Re those workarounds:
Does not work for nested types. The interface Config {
collections: Array<{name: string; folder: string; fields: Field[]}>
backend: {repo: string; branch: string}
}
type Field = {name: string; translate?: boolean} & (
| {widget: 'string'}
| {widget: 'markdown'}
| {widget: 'number'}
| {widget: 'object'; fields: Field[]}
| {widget: 'list'; field: Field}
| {widget: 'list'; fields: Field[]}
| {widget: 'select'; options: string[]}
| {widget: 'code'}
// | ...many more options
) Where there are lots of complex field types in the
This works, in that it gives errors for invalidly-defined values, but the reason I made this issue is I want to be able to take advantage of One other workaround interface User {
name: string
}
const john = {
name: 'John',
// @ts-expect-error
email: 'john@gmail.com',
birthDate: '2000-01-01',
} satisfies User Since luckily, the compiler only looks for the first excess property. But it seems pretty strange that typescript would push users into doing something like this. Edit: scratch that: doesn't quite work. The compiler reports no errors at all after the first excess property one. You do get autocomplete though so still arguably the least bad. Having said that, I do understand that there was a reason to disallow excess properties by default as explained in #47920. I've retitled this issue to "Make |
If you're writing this construct exactly once (or some very small number of times) in your codebase, which it sounds like you are, I think it's just way better to write the slightly awkward variant: const config = { /* huge thing */ } as const;
config satisfies Config; You won't get error spans inside the object literal, but the error message will give you an unambiguous key path to the offending property if the literal is
Of course, but generally we don't want to "spend" syntax unless it's absolutely critical to the language. |
satisfies
configurable to allow excess propertiessatisfies
configurable to allow excess properties in nested values
Yes there's only one config, but it's updated frequently. So it isn't just error spans, it's getting autocomplete on
I don't know if I can make the case that it's absolutely critical to the language. But without a way to do this it does seem like perfectly-innocent users doing legitimate things are being forced to choose between bad DX and writing bad code like ts-expect-error. |
I admit I'm a bit amused by this issue - it seems that the majority of people reporting issues with excess property checks think these checks are a core part of the type system and expect them to be much more comprehensive than they are (hence #12936), while you actually want a way to relax them. π |
Yes. We are large, we contain multitudes. |
Ah nice! const config = { /* huge thing */ } as const;
config satisfies Config;
// is much better than
;(x: typeof config): Config => x Concerning the autocomplete, what about this: type Config = { /* huge thing */ }
const Config = <TConfig extends Config>(config: TConfig) => config
const config = Config({ notInConfig: 42 }) |
from the linked issue:
the current behavior is confusingly inconsistent with how typescript works in general, and it didn't need to uniquely answer the above question in the first place, because typescript already did when deciding how passing arguments to functions would work. it fails the basic intuition of how "satisfies" ought to behave:
respectfully, it seems that over-thinking this led to an implementation where only a subset of relationships fitting that basic definition of X don't upset the compiler. this strikes me as a situation where people with legacy code or preferences to protect should enable a compiler flag, but the semantics ought to be fixed so that X is as broad as the above formulation defines it. no errors: type Foo = {foo: string}
const foo = {foo: 'bar', hello: 'world'}
const getFoo = (f: Foo): void => {}
getFoo(foo) error type Foo = {foo: string}
const foo = {foo: 'bar', hello: 'world'} satisfies Foo // error
const getFoo = (f: Foo): void => {}
getFoo(foo) this behavior is inconsistent with the rest of the language and astonishing in a bad way, regardless of how useful some may find it. |
@dkrieger consider if you wrote something like this interface Dimensions {
width: number;
height?: number;
depth?: number;
}
const p = { width: 20, detph: 20 } satisfies Dimensions; The only thing saving you here is excess property checks (EPC). Not erroring on this line is indefensible IMO - if There's a directionality problem here - if |
Suggestion
π Search Terms
satisfies + (known properties, unknown properties, excess)
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
When using
satisfies
, the value should be allowed to have extra properties.π Motivating Example
Satisfy
is defined this way when I Google it:Right now, the
satisfies
operator does more than that. It's more likesatisfies and goes no further than
. Example:In the above, the value of
john
clearly does satisfy the interfaceUser
, but the compiler complains:The first line of this is straight-up untrue! And it'd be really useful to be able to say "I have an object which I know is assignable to
User
, but it also has some specific extra stuff which I still care about.π» Use Cases
This first arose writing a very large, nested, complex config for a CMS. Right now it's a huge
as const
expression, which we have to carefully align with a typeConfig
- but the type only defines some of the properties a config can have at compile time. The value is later passed to a function which expects a validConfig
so we see invalid configuration errors in the wrong place. We have tried to usesatisfies
but hit the excess properties error for those properties not defined on theConfig
type. It's nested so we can't dosatisfies Config & Record<string, unknown>
except for top-level properties.The text was updated successfully, but these errors were encountered: