TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

satisfies operator for generic type constraints

Open manucorporat opened this issue 2 years ago • 4 comments

Suggestion

I would love to use the new satisfies in generic type constrains instead of extend because of its ability to detect the conformance of a type with a wider one.

🔍 Search Terms

satisfies, generic, constrains, extend,

✅ Viability Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

📃 Motivating Example

We need to make sure one type is JSON serializable, thanks to recursive types this is now possible, however we also want developers to specify an exact interface for strong types. Today, there is not easy way to check if the provided interface is JSON serializable!

// Let's assume that we expect that only data which is JSON serializable is a valid response.
type JSONValue =
    | string
    | number
    | boolean
    | undefined
    | null
    | { [key: string]: JSONValue }
    | Array<JSONValue>;


export interface MyData {
    foo: string,
    thisShouldBeAnError?: Date;
}

type FactoryFunction<RET satisfies JSONValue> = () => RET;

export const problematicFactory: FactoryFunction<MyData> = () => {
    return {
        foo: 'ok', 
        thisShouldBeAnError: new Date(),
    };
}

💻 Use Cases

  • Provide even better types when right now any is the only option
  • Values that can be serializable, JSON, SQL, any complex recursive type
  • Simplified types and less typing since the satisfied operator is part of the generic type

manucorporat avatar Nov 29 '22 08:11 manucorporat

It sounds like a duplicate of #14520 to me.

MartinJohns avatar Nov 29 '22 08:11 MartinJohns

Maybe! but with the introduction of the satisfies operator, we already have a semantic of how this could be implemented

manucorporat avatar Nov 29 '22 13:11 manucorporat

This doesn't look like a lower bound. Actually, I'm not sure what it is.

@manucorporat could you clarify what you're trying to do? Even without thisShouldBeAnError, a value of type MyData does not satisfy JSONValue, due to #15300:

interface MyData {
    foo: string,
}

declare const myData: MyData

const oops = myData satisfies JSONValue; // error!

playground link

What, specifically, would T satisfies U accomplish that T extends U does not?

jcalz avatar Nov 29 '22 15:11 jcalz

sorry for the confusing example, this example means to detect the error! extends does not work because JSONType is much wider.

extends respond to the questions: is type X a superset of Y? satisfies respond to does X conform to Y?

https://twitter.com/nomsternom/status/1597637566714417152?s=20&t=xPIUEvb_0-SsGqRRqO89MA

manucorporat avatar Nov 29 '22 18:11 manucorporat

X extends Y means that X is a subset, not a superset.

extends and satisfies mean the same thing: “X is assignable to Y”. The only difference is the LHS of extends is a type while for satisfies it’s a value.

fatcerberus avatar Nov 29 '22 18:11 fatcerberus

This isn't well-specified enough to be actionable. It seems like you're asking for either negated types, type parameter upper bounds, or possibly exact types, all of which have open issues already.

RyanCavanaugh avatar Dec 02 '22 01:12 RyanCavanaugh

…or possibly even oneof-style constraints.

fatcerberus avatar Dec 02 '22 01:12 fatcerberus

Some thing likes Opaque Result Types in Swift

galenjiang avatar Mar 03 '23 09:03 galenjiang

I'm running into this same problem - I'm not sure if there's even a way to check what I want right now. Here's a TS playground example

type AllowedValueType = string | number
type AllowedValueRecord = Record<string, AllowedValueType>

type AllowedValuesFunction<T extends AllowedValueRecord> = (values: T) => void

// ----- //

interface MyProps {
    message: string
    value: number
    isAllowed?: boolean
}

interface OtherProps {
    yes: "I am a string"
    yup: boolean
}

const thisShouldBeFine: MyProps = {
    message: "hello",
    value: 1
} satisfies AllowedValueRecord

const thisShouldNotBeFine: MyProps = {
    message: "hello",
    value: 1,
    // @ts-expect-error
    isAllowed: false,
} satisfies AllowedValueRecord

// @ts-expect-error Should fail because `isAllowed` is a boolean
const notAllowed: AllowedValuesFunction<MyProps> = () => {}

// This should be allowed
const allowed: AllowedValuesFunction<OtherProps> = () => {}
                                     // ^ I basically just want this to check satisfies

hiddenist avatar Sep 20 '23 01:09 hiddenist

X extends Y means that X is a subset, not a superset.

@fatcerberus Is this true? If X were a superset wouldn't that entail { a: number } extends { a: number, b: number } is true?

Or am I somehow confusing my definitions here?

sinclairnick avatar May 21 '24 05:05 sinclairnick

Yes, it’s true. Subset vs. superset are in terms of which values are assignable to it, not in terms of their properties. { a: number, b: number } is a subset of { a: number } because the latter consists of only those members of the former that also have a numeric b property.

(I suspect you may have misread that @sinclairnick - I’m specifically saying there that X is not a superset.)

fatcerberus avatar May 21 '24 14:05 fatcerberus

I see.So superset and subset in this context have a different definition to the strictly mathematical one - or at least how I interpret it.So in math we might say {2} is a subset of {2,3}, in TS I was thinking in terms of properties as members of the set. Instead you’re saying the correct interpretation is that a subset is “that which can be assigned to a type”?So since {2,3} is assignable to {2}, it would be a subset, in typescript parlance?Have I got this right?On 22 May 2024, at 2:16 AM, Bruce Pascoe @.***> wrote: Yes, it’s true. Subset vs. superset are in terms of which values are assignable to it, not in terms of their properties. { a: number, b: number } is a subset of { a: number } because the latter consists of only those members of the former that also have a numeric b property. (I suspect you may have misread that @sinclairnick - I’m specifically saying there that X is not a superset.)

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

sinclairnick avatar May 21 '24 21:05 sinclairnick

See this q/a. Think of a type as the set of values that have that type. Then a subtype corresponds to a subset, and a supertype corresponds to a superset. If, instead, you think of object types as their sets of declared properties, then you'll be thinking of things "backwards" because keyof is contravariant.

jcalz avatar May 21 '24 21:05 jcalz