Description
Suggestion
satisfies
should work in type
declarations, similar to how it works currently.
i.e. similar to
const x = y satisfies Z;
we could have
type X = Y satisfies Z;
🔍 Search Terms
satisfies, type
✅ 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.
📃 Motivating Example
Say there is some library, 'to-upper', that deals with lower case letters, both Roman and Greek.
// external library 'to-upper@1.0.0'
export type LowercaseChar =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's'
| 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'α' | 'β' | 'γ' | 'δ' | 'ε' | 'ζ' | 'η' | 'θ' | 'ι' | 'κ' | 'λ'
| 'μ' | 'ν' | 'ξ' | 'ο' | 'π' | 'ρ' | 'σ' | 'τ' | 'υ' | 'φ' | 'χ' | 'ψ' | 'ω';
export type UppercaseChar = ...;
export const toUpper = (lower: LowercaseChar): UppercaseChar => ...
And I want to use this library, but I actually only need to deal with a type that is narrower than LowercaseChar
. I only need to deal with vowels. So I make my type,
// my-types.ts
export type LowercaseVowel = 'a' | 'e' | 'i' | 'o' | 'u' | 'α' | 'ε' | 'η' | 'ι' | 'ο' | 'ω' | 'υ';
and then I use it with 'to-upper'
// my-app.ts
import { lowerToUpper } from 'char.js';
import type { LowercaseVowel } from './my-types';
const myLowercaseVowel: LowercaseVowel = 'ω';
toUpper(myLowercaseVowel);
All good.
However, now the maintainer of "to-upper" decides they don't want to deal with Greek characters anymore, so they make a breaking change. Being diligent and considerate of their users, they update the LowercaseChar
type definition as such:
// external library 'to-upper@2.0.0'
export type LowercaseChar =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's'
| 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
...
And I update my dependencies to to-upper@2.0.0.
It's true that my code will break on type checking, but it will fail where I've called toLower()
, because my LowercaseVowel
(which includes lowercase Greek characters) no longer satisfies the parameter of toLower()
, LowercaseChar
, which doesn't.
What would be preferable is if I had defined LowecaseVowel
explicitly with the constraint that it satisfies LowecaseChar
, i.e.
import type { LowercaseChar } from 'char.js';
type LowercaseVowel = 'a' | 'e' | 'i' | 'o' | 'u' | 'α' | 'ε' | 'η' | 'ι' | 'ο' | 'ω' | 'υ' satisfies LowercaseChar;
(Using the syntax suggested.)
If this were supported, I can see in the type declaration whether or not my narrow type satisfies the broader type.
💻 Use Cases
Similar to the above example, this could be used to narrow usages of overly broad types like any
in third-party libraries, e.g.
// third-party library
export type Payload = { data: any };
export function handlePayload(payload: Payload) {
...
}
// my satisfying library
import { type Payload, handlePayload } from 'third-party-library';
export type SpecialPayload = { data: { foo: string } } satisfies Payload;
export function handleSpecialPayload(specialPayload: SpecialPayload) {
handlePayload(specialPayload);
...
}
// consumer of my library
import { type SpecialPayload, handleSpecialPayload } from 'satisfying-library';
const mySpecialPayload: SpecialPayload = { data: { foo: 'bar' } };
handleSpecialPayload(mySpecialPayload);
In this particular contrived example, the same thing could be done with using interfaces interface SpecialPayload extends Payload { data: { foo: string; } }
, but I'm sure you could think of more complex examples where interfaces cannot be used.