Skip to content

satisfies T as type annotation #52195

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

Closed
5 tasks done
uhyo opened this issue Jan 11, 2023 · 7 comments
Closed
5 tasks done

satisfies T as type annotation #52195

uhyo opened this issue Jan 11, 2023 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@uhyo
Copy link
Contributor

uhyo commented Jan 11, 2023

Suggestion

πŸ” Search Terms

satisfies type annotation return impl trait

βœ… 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

Add a new syntax satisfies T (where T is a type) that can be used at positions where a type annotation is expected.

// As a type annotation for const
const magicNumber: satisfies number = 42;

// As a return type annotation
function getMagicNumber(): satisfies number {
  if (Math.random() < 0.5) {
    return 42;
  } else {
    return 0;
  }
}

The above code should have the same meaning as the following code that utilizes TypeScript 4.9's exp satisfies T syntax.

// Above code in currently supported syntax
const magicNumber = 42 satisfies number;

function getMagicNumber() {
  if (Math.random() < 0.5) {
    return 42 satisfies number;
  } else {
    return 0 satisfies number;
  }
}

The behavior of the new satisfies T type annotation syntax can be summarized as follows:

  • Final type of a satisfies T annotation is still left to inference.
  • When a variable declaration has a satisfies T annotation, the satisfies T constraint is applied to its initiator.
  • When a function's return type has a satisfies T annotation, the satisfies T constraint is applied to all return statements in it.

πŸ€” How about function parameters?

Function parameter could have a satisfies T annotation but it doesn't make any additional sence. If it is allowed anyway, it should be have the same as just T.

function useNum(num: satisfies number) {
  // ...
}

πŸ“ƒ Motivating Example

People have found TS 4.9's exp satisfies T syntax very useful, especially when one wants to explicitize a simple constraint against a variable and get contextual typing for its initializer, while also preserving its precise type for further type calculation.

For ease of understanding what a variable is meant for, its type constraint should be put as close as possible to the declaration of variable. The ordinary type annotation plays very well in this view.

type NumberClassifier = (num: number) => string;

const classifyToOddAndEven: NumberClassifier = (num) => {
  if (num % 2 === 0) {
    return "even";
  } else {
    return "odd";
  }
}

However, in order to extract more sophisticated information through type inference, we resort to the exp satisfies T syntax, which results in distance between variable definitiion and constraint: (note: assume we need contextual typing too, so that either type annotation or satisfies is necessary.)

const classifyToOddAndEven = ((num) => {
  if (num % 2 === 0) {
    return "even";
  } else {
    return "odd";
  }
}) satisfies NumberClassifier;

The proposed syntax has benefit of both ordinary type annotations and TS 4.9 exp satisfies T syntax.

const classifyToOddAndEven: satisfies NumberClassifier = (num) => {
  if (num % 2 === 0) {
    return "even";
  } else {
    return "odd";
  }
};

You might think that inlay hints help recognizing the type of classifyToOddAndEven, but actual types are often too complex to be fully shown, nor as descriptive as satisfies constraints. In practical situations actual types are only for type-level culculation and nor for human recognization.

Inlay hint for the type of classifyToOddAndEven isn't shown completely.

The satifies T annotation for return type is also proposed for similar motivation.

Instead of applying the satisfies T constraint to the whole function type, just constraining its return type is sometimes enough.

const classifyToOddAndEven = (num: number): satisfies string => {
  if (num % 2 === 0) {
    return "even";
  } else {
    return "odd";
  }
};

This is still preferable to the existing syntax that we don't have to repeat the satisfies constraint.

Another small motivative example: Playground (thanks @progfay!)

πŸ’» Use Cases

My actual use case in mind is about writing GraphQL resolvers.

Thanks to Graphql Code Generator we can generate type defintion for resolvers. However, generated type definition are too permissive because it allows several different ways of writing resolvers. As a result, further type-level calculation using the generated type is hard.

import type { User, UsersResolver } from './generated-type-definitions';

const usersResolver: UsersResolver = async () => {
  const usersFromDb: User[] = /* ... access database ... */
  return usersFromDb;
};

type ResultType = ReturnType<typeof usersResolver>;
// ↑ Promise<User[]> is ideal, but actual type is Promise<(User | Promise<User>)[]>

With the proposed syntax, calculation of ResultType in the above example can be improved.

import type { User, UsersResolver } from './generated-type-definitions';

const usersResolver: satisfies UsersResolver = async () => {
  const usersFromDb: User[] = /* ... access database ... */
  return usersFromDb;
};

type ResultType = ReturnType<typeof usersResolver>;
// ↑ Promise<User[]> πŸ’•

The refined ResultType calculation then would be useful for unit testing where we want to use the knowledge that usersResolver returns a Promise<User[]>, not Promise<(User | Promise<User>)[]>.

Note again: use of satisfies in any form is necessary in above examples, as we want to heavy-use contextual typing when writing actual GraphQL resolvers.

🎨 Prior Art

The proposed feature is essentially the same as Rust's impl Trait. In Rust, actual type tends to become very complex and even impossible to write down as code. impl Trait acts as a descriptive placeholder.

@RyanCavanaugh
Copy link
Member

This seems like effectively a duplicate of #51556, since hypothetically you would write

function getMagicNumber() {
  if (Math.random() < 0.5) {
    return 42;
  } else {
    return 0;
  }
} satisfies (...args: any[]) => number;

using that feature. #51556 also addresses a broader class of use cases (e.g. simultaneously constraining the parameter types as the return type) without introducing a type annotation syntax that only makes sense in a very small (1?) number of places.

@RyanCavanaugh
Copy link
Member

BTW, why isn't

const usersResolver: satisfies UsersResolver = async () => {
  const usersFromDb: User[] = /* ... access database ... */
  return usersFromDb;
};

already solved by

const usersResolver = async () => {
  const usersFromDb: User[] = /* ... access database ... */
  return usersFromDb;
} satisfies UsersResolver;

? What would the difference between these forms be, if any?

@uhyo
Copy link
Contributor Author

uhyo commented Jan 12, 2023

What would the difference between these forms be, if any?

The point would be that the variable definition const usersResolver and its constraint satisfies UsersResolver becomes closer to each other with the proposed syntax. If a function is large, you can't see other important information (function name, parameter name, etc.) and the satisfies constraint at a grance if the satisfies constraint comes after function body.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jan 12, 2023
@RyanCavanaugh
Copy link
Member

While I agree with that as being an upside, I don't think it merits the trade-off compared to making a type psuedo-operator that only works in certain places. Tracking the use case at #51556

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Jan 12, 2023
@kaya3
Copy link

kaya3 commented Jan 27, 2023

I, too, would like to see const x: satisfies T = ...; be allowed. I have some large object literals where I want to check that every value has type V without losing the type information about the keys (so const x: Record<string, V> wouldn't work, because then I can't declare type K = keyof typeof x). As I understand, one of the primary purposes for the satisfies operator is to replace use-cases like this where a generic identity function is otherwise needed; so the consequence of moving the type annotation from the first line of the declaration to the end of it is unfortunate:

const x = (<K extends string>(obj: Record<K, V>) => obj)({
    // hundreds of lines of code
});

// becomes:

const x = {
    // hundreds of lines of code
} satisfies Record<string, V>;

I disagree that the linked issue is a duplicate, because it would leave the type information at the end of the declaration where it is less likely to be seen, and would do nothing for declarations that aren't functions.

I understand that the expr satisfies T syntax is consistent with how the as operator is written, but personally I don't think of satisfies as a checked as, I think of it as a non-widening type annotation. Syntactically speaking, x: satisfies T would have the same status as x!: T or x?: T. You could make the same argument that ! and ? here are "pseudo-operators that only work in certain places"; likewise, it makes no sense to write function foo(x!: T) {} or const x?: T;, and those are syntax errors.

@uhyo
Copy link
Contributor Author

uhyo commented Feb 5, 2023

For anyone who supports this proposal, here is a TypeScript Language Service Plugin that might satisfy you... Hope it helps.

https://github.com/uhyo/ts-satisfactory-plugin

@btoo
Copy link

btoo commented Aug 11, 2024

i'm humbly (as someone who's not himself a maintainer of TypeScript) requesting that this proposal be re-opened for consideration on the grounds that #52195 (comment) 's

type psuedo-operator that only works in certain places

already exists in TypeScript in other forms (if i'm interpreting it correctly - e.g. type predicates) while prior arts such as Swift's some operator (which is usable on variable declarations, function parameters, and return types) challenge the idea that we shouldn't think a similar feature in TypeScript

merits the trade-off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants