-
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
"satisfies" operator to ensure an expression matches some type (feedback reset) #47920
Comments
If you discard safe upcast you shouldn't probably close #7481 with the current issue as a solution as it was only about safe upcast which somehow morphed into discussion about something else. I totally see the value in the other scenarios, but it seems to be a different feature. EDIT: now after some thinking I think you are right there are not many scenarios left where safe upcast is needed if |
Agree it should be Can't we expect people to use I don't think the approach to type Tuple = [number, number];
function tackOneOn(tuple: Tuple){
tuple.push(4)
return tuple;
}
const mutated: Tuple = [3,4]
const returned = tackOneOn(mutated); // also Tuple, refers to mutated, but is 3 long
const howMany = returned.length // type 2 |
I totally agree with discarding the safe upcast case in favor of the other scenarios. As far as using What I don't really understand well enough is how exactly contextual typing comes into play here. Like, I think the contextual typing process could allow us to incorporate some information from the type |
Contextual typing will set The fact that parameters get their types from contextual typing but arrays don't is a sort of barely-observable difference (today) that this operator would make very obvious, hence the PR to change empty arrays. |
@RyanCavanaugh Thanks. Gotcha. That seems like a good change then, almost independent of what happens with |
For excess property checking... I honestly have no idea. But I'm a bit confused about how const origin = {
x: 0,
y: 0,
z: 0 // OK or error?
} satisifes Point; while const car = {
start() { },
move(d) {
// d: number
},
stop() { }
} satisfies Moveable; |
I wasn't expecting any Contextual Typing at all arising from This would clutter my comprehension of the likely consequences of the operator. I was expecting Before reading this aspect of the proposal I expected to fulfil all the requirements of the type system when declaring or assigning (using the normal tools), but |
Consider this example: const car = {
start() { },
move(d) {
// d: number
},
stop() { }
} satisfies Moveable; Without contextual typing, |
This may come across as a bit of a non-sequitur, but, if we're thinking about excess property checking rules, it also seems like we should make sure that the ultimate design for With Of course, editor assistance features can't fully replace excess property checks; each has clear strengths and limits. But I do think the same concerns that motivate checking for excess properties with I'd hate to land on a design that somehow makes an automatic refactor like this hard to implement: export type Color = { r: number, g: number, b: number };
export const Palette = {
white: { r: 255, g: 255, b: 255},
black: { r: 0, g: 0, b: 0},
blue: { r: 0, g: 0, b: 255 },
} satisfies Record<string, Color>; Here, I think I should be able to rename Perhaps it's roughly-equally easy to support that kind of IDE behavior for any of the designs we're considering here, in which case this really is orthogonal/can be ignored in this thread. But I'm just raising it because I have no idea how that language server stuff works. |
Up until recently I was in the " interface ApiResponseItem {
}
interface ApiResponse {
[ index: string ]: string | number | boolean | ApiResponseItem | ApiResponseItem[]
}
let x : ApiResponse = {
items: [ { id: 123 } ],
bob: true,
sam: 1,
sally: 'omg',
}
console.log(x);
class Test {
private result : boolean | undefined;
doIt () {
let person : ApiResponse = {
bob: true
};
this.result = person.bob; // <-- error here
}
} Presumably the person who wrote this code wanted to ensure upfront that the object literal was a valid |
@RyanCavanaugh I like the idea, but have you considered |
Hey, thanks @RyanCavanaugh for considering my feedback! Here’s some thoughts why Contextual Typing might not be desirable in my view. DEFEATS PURPOSE Assuming the intent that const car = {
start() { },
move(d) {},
stop() { }
} satisfies Moveable; EXPLANATORY SIMPLICITY, LEAST SURPRISE I want to guide colleague adopters of Typescript with simple statements about the behaviour of the language. As per other comments in this thread, adopters often seem to think that ‘as X’ is a suitable workaround to ensure typing when it is normally exactly the opposite. It would be easy to guide them towards typing the structure with all the pre-existing tools then use Explaining to them that
To give an idea of how the Contextual Typing augmentation tripped me up, this was as if I told you that `expect(value).toBe(true) would actually set the value in some circumstances (because you said you expected that) ! Having this dual role makes it a different category of operator to my mind. Following the OCKAM’S RASOR When I have hit the ‘dead ends’ which required this operator, the feature which wasn’t available in any other way was a form of type-checking without typing for which no mechanism existed at all. By contrast, blending in a Contextual Typing feature here isn’t driven by necessity, as all the typing you describe can be easily achieved in other ways (including existing Contextual Typing mechanisms). In the case you shared we would simply have to type const car = {
start() { },
move(d: number) {},
stop() { }
} satisfies Moveable; I speculate if this is more likely in any real case anyway and would explicitly benefit from the existing Contextual Typing feature const car: Moveable & Service = {
start() { },
move(d) {},
stop() { }
}; TYPE LOCALISATION PRACTICE Maybe worth noting some community practice even tries to force types to be explicit when they can be inferred. So I believe some would see concrete benefit from seeing SUMMARY Based on a minimal model of what satisfies could do (it checks if something is satisfied) having it actually add types to I accept that others may have a more nuanced model and won’t need the operator to be cleanly validating with no 'side effects'. Looking forward to seeing how the feature develops! |
@RyanCavanaugh I'm not sure how you picture the following examples when you go with the contender const a = [1, 2] satisfies [number, number];
const b = [1, 2] satisfies [unknown, unknown];
const c = [1, 2] satisfies [unknown, 2];
const d = {a: 1, b: 2} satisfies {a: unknown, b: 2}; If you assign
|
Resurfacing #7481 (comment) here, though I believe my suggestion has been mentioned more in the past |
@kasperpeulen good questions, and ones I should have put in the OP. Arrays contextually typed by a tuple type retain their tupleness, and numeric literals contextually typed by a union containing a matching numeric literal type retain their literal types, so these would behave exactly as hoped (assuming contextual typing is retained). A neat trick for trying these out in existing syntax is to write // Put the desired contextual type here
declare let ct: {a: unknown, b: 2}
// Initialize with assignment expression
const d = ct = {a: 1, b: 2};
// d: { a: number, b: 2} Chained assignment is AFAIR the only place where you can capture the inferred type of a contextually-typed expression. |
@RyanCavanaugh I have read the proposal a couple of times, and all makes sense to me, except this point:
One of your examples would also not work if you would disallow excess properties: const car = {
// TS2322: Type '{ start(): void; move(d: number): void; stop(): void; }' is not assignable to type 'Movable'.
// Object literal may only specify known properties, and 'start' does not exist in type 'Movable'.
start() {},
move(d) {
// d: number
},
stop() {},
} satisfies Moveable; There are two reason why disallowing excess properties doesn't feel right to me.
class Car implements Movable {
start() {}
move(d: number) {
}
stop() {}
} For both
const car2: Moveable = {
start() {},
move(d) {
// d: number
},
stop() {},
}; As even if car is a subtype of // TS2339: Property 'start' does not exist on type 'Movable'.
car2.start(); However, when we only want to satisfy "Moveable", but also purposely wanting to be more than that, we can access those extra properties just fine: // fine
car.start(); I think disallowing excess properties when using type Keys = 'a' | 'b' | 'c';
// Property 'd' might be intentional excess *or* a typo of e.g. 'b'
const defaults = { a: 0, d: 0 } satisfies Partial<Record<Keys, number>>;
declare function foo(object: Record<Keys, number>);
// hey why do I get an error here, I thought b had a default, oh wait, I made a typo there
const bar = foo({...defaults, c: 1); Also, you won't get autocompletion for |
Would it also be possible to use the proposed type DoubleNumberFn = (a: number) => number
function doubleNumber(num) {
// both num and doubled are numbers instead of any
const doubled = num * 2
// TS2322 Type 'string' is not assignable to type 'number'.
return doubled.toString()
} satisfies DoubleNumberFn This would potentially address the feature request in #22063 |
My usecase is fairly simple. Consider this possibly dangerous situation: By using My ideal solution would be using getFirebaseRef().push({} is MyItem) -> now I would get hints for object properties Which is practically equivalent to this: const x: MyItem = { // Type '{}' is missing the following properties from type 'MyItem': another, id, param
hi: 1, // Type '{ hi: number; }' is not assignable to type 'MyItem'. Object literal may only specify...
}
getFirebaseRef().push(x) from #7481 (comment) |
+1 to the sentiments repeated several times already that a To that end, I think that many of the scenarios in the OP are off-topic. The original summary explicitly states that basically all scenarios for a const a: Movable = { … };
// is the same as
const a = { … } satisfies Movable; I think this is exactly right. There is no type operator or keyword that is equivalent to this and I think that's what is missing. The motivation for a Let me provide some further examples, similar to what others have posted already: interface Component {
id: string;
name: string;
// … other properties
}
// GOAL: Have a master list of all our components and some associated info/config/whatever
// Secondary goal: don't allow extra properties. I personally think it is a source of bugs.
// Attempt A: Index signature type annotation.
// Result: UNDESIRED. No type safety for accessing properties on `componentsA`
const componentsA: { [index: string]: Component } = {
WebServer: { id: '0', name: "Web Server" },
LoadBalancer: { id: '1', name: "Load Balancer" },
Database: { id: '1', name: "Load Balancer", url: "https://google.com" }, // DESIRED type error. `url` is extra
};
console.log(componentsA.NotARealComponent); // UNDESIRED because no type error. `componentsA` allows dereferencing any property on it
// Attempt B: Using `as` operator
// Result: UNDESIRED. Missing or extraneous properties on components
const componentsB = {
WebServer: { id: '0', name: "WebServer" } as Component,
LoadBalancer: { id: '1' } as Component, // UNDESIRED because completely missing property `name` (NO IDEA why this compiles at-present)
Database: { id: '1', name: "Load Balancer", url: "https://google.com" } as Component, // UNDESIRED because no type error for extraneous `url` property
};
console.log(componentsB.NotARealComponent); // DESIRED type error. No property `NotARealComponent`
console.log(componentsB.LoadBalancer.name); // UNDESIRED because `name` property does not even exist on `LoadBalancer`
// Attempt C: Using a Type<T>() function
// Result: DESIRED. But there is no way of doing this with the type system - must invoke runtime identity function
function Type<T>(obj: T): T { return obj; }
const componentsC = {
WebServer: Type<Component>({ id: '0', name: "WebServer" }),
LoadBalancer: Type<Component>({ id: '1' }), // DESIRED type error. Property `name` is missing.
Database: Type<Component>({ id: '1', name: "Load Balancer", url: "https://google.com" }), // DESIRED type error. Property `url` is extra.
};
console.log(componentsC.NotARealComponent); // DESIRED type error. No property `NotARealComponent` I desire an operator that is the equivalent of the // Common interface
interface ComponentQuery {
name: string;
}
// For querying for databases specifically
interface DbComponentQuery extends ComponentQuery {
type: "db";
shardId: string;
}
// etc… presumably other specific queries too
// Query for a component or something, IDK.
// Would return a `Component` in the real world.
// Just a contrived example.
function queryForComponent(component: ComponentQuery): void { /* … */ }
// GOAL: Call `queryForComponent()` for a DB component
// Attempt A: No type casting
queryForComponent({
type: "db", // UNDESIRED because type error that `type` does not exist on `ComponentQuery`
name: "WebServer",
shardId: "2",
});
// Attempt B: `as` keyword
queryForComponent({
type: "db",
name: "WebServer",
// UNDESIRED: Missing `shardId` property - not at all useful
} as DbComponentQuery);
// Attempt C: Type<T>() function
function Type<T>(obj: T): T { return obj; }
// DESIRED. Will not work if any property is missing, extra, or incorrect type.
queryForComponent(Type<DbComponentQuery>({
type: "db",
name: "WebServer",
shardId: "2",
}));
// Only working alternative. Declare variable just to pass into function.
// Not always possible in certain difficult scenarios.
const query: DbComponentQuery = {
type: "db",
name: "WebServer",
shardId: "2",
};
queryForComponent(query); Apologies for the long and rather in-depth examples. I hope they will be useful to clarify the need here, and allow others to simply +1 this instead of needing to provide further similar examples. I understand that all the scenarios described in the OP are real problems faced by people, and may or may not need addressing, but I believe the behavior I have described here is desired by many, and I feel that the OP and the conversation within this thread are explicitly heading towards removing it from the scope. Even if this Lastly I will say, if your desires are described in this comment, I would ask you to 👍 it. It will help keep the thread tidy while also demonstrating how much the community desires this behavior. |
@peabnuts123 You say " I think your examples actually show why limiting // This has all the same safety as your componentsC example,
// modulo some open questions about excess property checks. And it has less repetition.
const componentsC = {
WebServer: { id: '0', name: "WebServer" },
LoadBalancer: { id: '1' } // satisfies rejects this for missing the name
Database: { id: '1', name: "Load Balancer", url: "https://google.com" }, // might or might not reject the extra url
} satisfies Record<string, Component>; // Again, same safety as your example, except for the open questions about excess property checks.
queryForComponent({
type: "db",
name: "WebServer",
shardId: "2",
} satisfies DbComponentQuery); I also wanna point out that the TS team realized that |
@ethanresnick Thanks for responding to my examples and showing how the proposed logic could work to achieve those goals. I think you're right to put aside concerns around extraneous properties at this point too, as I get the sense that the community is divided on this matter. I included those concerns as relevant in my examples but I am happy to put them aside; I will say however that before anything like this lands in TypeScript, those concerns around extraneous properties need to be addressed and an informed decision made. As for what evidence I have for my assumption, it seems clear to me that almost everybody in the original thread is asking for it. I have been following it for a while and perhaps in the middle of the thread the topic changes, but for example, from the first 10-20 replies there are many examples describing exactly the same thing, which to me appears to be out-of-scope for the proposal in this thread. Perhaps I am missing some nuance? I only know of up/down casting from strongly typed languages like Java and C# where an upcast is always safe and a downcast may lead to a runtime exception. TypeScript's semantics around upcasts and downcasts (might be missing properties) are somewhat mysterious to me (see "I don't know why this even compiles" comment in my previous example). For clarity, my assumption is that most people seem to desire an operator essentially equivalent to: function Is<T>(obj: T): T { return obj; } I have realised that not explicitly passing const myThing = Is<Thing>({
a: 2,
b: 3,
}); where Here are examples of what I see as people asking for this functionality:
I feel personally that the use of such an operator in your first counter-example ( Again I'd like to restate that I may be missing some nuance here as creating language and type systems is Hard™ so please forgive me if I'm missing the mark. |
Probably worth keeping in mind that people will probably ask for something like |
In a world where
In a world where
One could make an argument that this means we need two operators, which, fine, but if you add We also have to consider the relative frequency of things; if you have const v: C = e; or function fn<T extends U>(arg: T): void;
fn<C>(e); then you already have a place to write |
I like the idea of having the strictness and the "castness" be orthogonal. For example, if we imagine the
Given a type like this type T = {
a: number;
b: bool;
} I would expect the following behaviour: Loose without cast // typeof data === { a: number, b: bool, c: string }
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
c: "text", // OK
} satisfies T;
// Error: missing property 'a' of 'T'
const data = {
b: true, // OK, gets auto complete
} satisfies T; Strict without cast // Error: 'T' doesn't have property 'c'
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
c: "text", // Bad
} strict satisfies T;
// Error: missing property 'a' of 'T'
const data = {
b: true, // OK, gets auto complete
} strict satisfies T;
// typeof data === { a: number, b: bool }
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
} strict satisfies T; Loose with cast // typeof data === T
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
c: "text", // OK
} is T;
// Error: missing property 'a' of 'T'
const data = {
b: true, // OK, gets auto complete
} is T; Strict with cast // Error: 'T' doesn't have property 'c'
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
c: "text", // Bad
} strict is T;
// Error: missing property 'a' of 'T'
const data = {
b: true, // OK, gets auto complete
} strict is T;
// typeof data === T
const data = {
a: 1, // OK, gets auto complete
b: true, // OK, gets auto complete
} strict is T; I know that this adds a lot of extra word reservation, and I'm not sure if that's against the wishes of the language design, but I think it conveys intent very clearly. |
* Prefer `as const` with TypeScript 4.9 introduced `satisfies` microsoft/TypeScript#47920 https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/#satisfies * `npm install --save-dev @typescript-eslint/[email protected] && npm install --save-dev @typescript-eslint/[email protected]` * `npm install --save-dev --legacy-peer-deps @typescript-eslint/[email protected] @typescript-eslint/[email protected]` * npm/cli#4998 * Need here too * Add TODO comments
…#724) * Prefer `as const` with TypeScript 4.9 introduced `satisfies` operator microsoft/TypeScript#47920 https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/#satisfies This is my hope since #479 (comment) * `npm run build` To includes babel 7.2.0 https://babeljs.io/blog/2022/10/27/7.20.0 * `npm i -g npm-check-updates && ncu -u` To bump jest to apply jestjs/jest#13199 (comment) * `npm install` * Revert "`npm i -g npm-check-updates && ncu -u`" This reverts commit 1131421. * Revert "`npm install`" This reverts commit 2dfc43e. * Specify TS 4.9 satisfies supported babel plugin with npm overrides https://babeljs.io/blog/2022/10/27/7.20.0 https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides vitejs/vite#7634 #724 (comment)
@ftonato (cc @josh-hemphill) note that you can forcibly narrow to a tuple by doing |
This would be a great feature to have. Mechanically-speaking, it should enforce the same behavior as the below: const something: SomeType = {...} //this object must conform to SomeType
function foo(param: SomeType) {}
foo({...}) //the passed object must conform to SomeType
function bar(): SomeType {
return {...} //the returned object must conform to SomeType
} Typescript is already quite powerful with type-casting, but aside from the above options, has a hard time with type enforcing. const something = {...} as SomeType //often fails due to typecasting
const somethingElse = <SomeType>{...} //same problem as with the above In many cases, these approaches require additional boilerplate code, making them cumbersome and less ergonomic for single-use or inline situations. Therefore, as this request already proposes, having a loose, "on the fly" type assertion is the way to go: export default {...} satisfies SomeType
//or
export default {...} is SomeType //retains the same keyword already enforced within typeguard function return value enforcement. Introducing a more concise way to enforce types would make TypeScript more developer-friendly and allow for more ergonomic solutions in cases where existing methods are too verbose. |
|
Funny that it does exist, when I couldn't find the root of it based on this issue, couldn't find any search engine results, and neither ChatGPT nor Bing had any info on it (obviously it's quite a new fix). So thank you for sharing! |
I found this thread looking for a way to make a function definition conform to a function type. Unless I missed something, it seems like that It's easiest to explain with a small example: type FuncType = (this: {x: number}, y: number) => number;
// OK!
const foo1: FuncType = function foo(y) { return this.x + y; };
// OK!
const foo2 = function foo(y) { return this.x + y } satisfies FuncType;
// Impossible to type?
//function foo(y) { return this.x + y; } I'm currently using the first form (because the second one seems strictly less clear) but I think it would be nice if it were possible to write the third version like this: function foo(y) satisfies FuncType { return this.x + y; } |
Feature Update - February 2022
This is a feedback reset for #7481 to get a fresh start and clarify where we are with this feature. I really thought this was going to be simpler, but it's turned out to be a bit of a rat's nest!
Let's start with what kind of scenarios we think need to be addressed.
Scenario Candidates
First, here's a review of scenarios I've collected from reading the linked issue and its many duplicates. Please post if you have other scenarios that seem relevant. I'll go into it later, but not all these scenarios can be satisfied at once for reasons that will hopefully become obvious.
Safe Upcast
Frequently in places where control flow analysis hits some limitation (hi #9998), it's desirable to "undo" the specificity of an initializer. A good example would be
The canonical recommendation is to type-assert the initializer:
but there's limited type safety here since you could accidently downcast without realizing it:
The safest workaround is to have a dummy function,
function up<T>(arg: T): T
:which is unfortunate due to having unnecessary runtime impact.
Instead, we would presumably write
Property Name Constraining
We might want to make a lookup table where the property keys must come from some predefined subset, but not lose type information about what each property's value was:
There is no obvious workaround here today.
Instead, we would presumably write
Property Name Fulfillment
Same as Property Name Constraining, except we might want to ensure that we get all of the keys:
The closest available workaround is:
but this assignment a) has runtime impact and b) will not detect excess properties.
Instead, we would presumably write
Property Value Conformance
This is the flipside of Property Name Constraining - we might want to make sure that all property values in an object conform to some type, but still keep record of which keys are present:
Another example
Here, we would presumably write
Ensure Interface Implementation
We might want to leverage type inference, but still check that something conforms to an interface and use that interface to provide contextual typing:
Here, we would presumably write
Optional Member Conformance
We might want to initialize a value conforming to some weakly-typed interface:
Optional Member Addition
Conversely, we might want to safely initialize a variable according to some type but retain information about the members which aren't present:
Contextual Typing
TypeScript has a process called contextual typing in which expressions which would otherwise not have an inferrable type can get an inferred type from context:
In all of the above scenarios, contextual typing would always be appropriate. For example, in Property Value Conformance
Contextually providing the
n
parameters anumber
type is clearly desirable. In most other places than parameters, the contextual typing of an expression is not directly observable except insofar as normally-disallowed assignments become allowable.Desired Behavior Rundown
There are three plausible contenders for what to infer for the type of an
e satisfies T
expression:typeof e
T
T & typeof e
*SATA: Same As Type Annotation -
const v = e satisfies T
would do the same asconst v: T = e
, thus no additional value is providedT
typeof e
T & typeof e
Discussion
Given the value of the other scenarios, I think safe upcast needs to be discarded. One could imagine other solutions to this problem, e.g. marking a particular variable as "volatile" such that narrowings no longer apply to it, or simply by having better side-effect tracking.
Excess Properties
A sidenote here on excess properties. Consider this case:
Is
z
an excess property?One argument says yes, because in other positions where that object literal was used where a
Point
was expected, it would be. Additionally, if we want to detect typos (as in the property name constraining scenario), then detecting excess properties is mandatory.The other argument says no, because the point of excess property checks is to detect properties which are "lost" due to not having their presence captured by the type system, and the design of the
satisfies
operator is specifically for scenarios where the ultimate type of all properties is captured somewhere.I think on balance, the "yes" argument is stronger. If we don't flag excess properties, then the property name constraining scenario can't be made to work at all. In places where excess properties are expected,
e satisfies (T & Record<string, unknown>)
can be written instead.However, under this solution, producing the expression type
T & typeof e
becomes very undesirable:Side note: It's tempting to say that properties aren't excess if all of the satisfied type's properties are matched. I don't think this is satisfactory because it doesn't really clearly define what would happen with the asserted-to type is
Partial
, which is likely common:Producing
typeof e
then leads to another problem...The Empty Array Problem
Under
--strict
(specificallystrictNullChecks && noImplicitAny
), empty arrays in positions where they can't be Evolving Arrays get the typenever[]
. This leads to some somewhat annoying behavior today:The
satisfies
operator might be thought to fix this:However, under current semantics (including
m: typeof e
), this still doesn't work, because the type of the array is stillnever[]
.It seems like this can be fixed with a targeted change to empty arrays, which I've prototyped at #47898. It's possible there are unintended downstream consequences of this (changes like this can often foul up generic inference in ways that aren't obvious), but it seems to be OK for now.
TL;DR
It seems like the best place we could land is:
T & Record<string, unknown>
)typeof e
as the expression type instead ofT
orT & typeof e
Does this seem right? What did I miss?
The text was updated successfully, but these errors were encountered: