Open MaximSagan opened 1 year ago
This is pretty unlikely to happen, since you can already write:
type Satisfies<T extends U, U> = T;
type Test = Satisfies<string, string | number>;
or
type Satisfies<T extends U, U> = T;
type Test = string;
type Check_Test = Satisfies<Test, string | number>;
None of the use cases that motivated satisfies
have good correspondences to type declarations.
@RyanCavanaugh
I was about to post a similar suggestion when I found this. I am unsure if it is different enough to warrant opening another issue. I hope it's ok to describe the gist of it here:
Basically I would like to apply satisfies
to interface definitions to make sure a type properly narrows another type (and get auto completion for property names).
Without satisfies
interface Broad {
property: string
}
interface NarrowA {
property: "foo"
}
//what if 'Broad' changes?
interface NarrowB {
propertyy: "bar"
//oops, typo
}
With satisfies
interface Broad {
property: string
}
interface NarrowA {
property: "foo"
} satisfies Broad
// will no longer compile if 'Broad' changed
interface NarrowB {
propertyy: "bar"
//get's caught
} satisfies Broad
However, I am aware this would only really be a shorthand for something like this:
interface BP<T extends string> {
property: T
}
type Broad = BP<string>
type NarrowA = BP<"foo">
type NarrowB = BP<"bar">
Should I even open a new issue for this? Of course I'd elaborate more there.
@RyanCavanaugh The satisfies type does not have the same semantics as satisfies:
type Satisfies<T extends U, U> = T;
type Test = Satisfies<{
hello: "world";
and: "universe"
}, { hello: string }>; // pass
const test = {
hello: "world";
and: "universe"
} satisfies { hello: string }; // don't pass
is there a way today in TypeScript to have the first one to not succeed ? i need this feature for Zodios library to detect users typo. example :
export const edge = new Zodios(
`/api/v1`,
[ // api definition is narrowed to preserve string literals with generic helpers behind the scene
{
method: "get",
path: "/results",
alias: "getResults",
paramaters: [ // typo, should be 'parameters', but typescript don't catch it since parameters are optional
{
type: "Query",
name: "z",
schema: z.string(),
},
],
response: z.string(),
}
],
);
and i don't want to force users to write this, it would be a really bad developper experience:
export const edge = new Zodios(
`/api/1`,
[ // api definition is narrowed to preserve string literals with generic helpers behind the scene
{
method: "get",
path: "/results",
alias: "getResults",
paramaters: [ // typo, is now catched since 'satisfies' don't allow excess properties
{
type: "Query",
name: "z",
schema: z.string(),
},
],
response: z.string(),
}
] satistifes ZodiosApiDefinitions,
);
I have another use case for this. Imagine you're defining some types associations starting from a type T:
type T = 'a' | 'b'
const VALUE_MAP = {
'a': "hello",
'b': 42
} satisfies Record<T, any> // ok
// here I can use VALUE_MAP['a'] to access the associated value
type TYPE_MAP = {
'a': "hello"
'b': 42
'c': "I can put whatever I want here"
} satisfies Record<T, any> // error, no satisfies with types
// here I can use TYPE_MAP['a'] to access the associated type
From what I can see there's no way to both A) constraint the keys of my TYPE_MAP
to be of type T
AND B) to associate a specific type (possibly different for every key) manually, without referencing other maps/associations/generic types (the latter would need satisfies
at the type level too btw)
I'm also interested in an addition with this, too.
My use-case is to be able to validate either a class, another interface, or an object against a type with an index signature, but without applying the index signature behavior itself.
The current language support almost exactly works like I need it to. The only issue is that the shape of a type cannot be checked against an index signature successfully, without adding the index signature to it. So, my goal is to validate that the keys on an object/class/interface/type should match that of an index signature type, without enabling indexing on the shape itself.
I wrote an example of doing with with the primitives and shapes which are valid within the JSON format:
// JSON primitive types
type JSONPrimitive =
| string
| number
| boolean
| JSONArray
| JSONObject
| null;
interface JSONArray<T extends JSONPrimitive = JSONPrimitive> extends Array<T> {}
interface JSONObject {
[key: string]: JSONPrimitive;
}
// Validate that my own types are compatible with that of the JSON format.
// This is the only way to currently check one interface against another.
interface MyDataStructure extends JSONObject {
hey: number;
}
// Possible syntax proposal to validate the type against the other type, without applying the index signature.
interface MyDataStructure satisfies JSONObject {
hey: number;
}
// --------------------------
const myObject: MyDataStructure = {
hey: 23
};
// @ts-expect-error
myObject.shouldNotBeIndexable;
// --------------------------
// shouldn't error, I would like to validate the class against the shape, not forcing it to have an index signature.
class MyDataStructureClass implements MyDataStructure {
constructor(public hey: number = 32) {}
}
// Similar syntax proposal for classes as to that of the validation for interfaces.
class MyDataStructureClass satisfies JSONObject {
constructor(public hey: number) {}
}
// --------------------------
function getValueForHey<T extends MyDataStructure>(myDataStructure: T): MyDataStructure["hey"] {
return myDataStructure.hey;
}
// also shouldn't error, I'd like to check that the shape of the class is valid against the index signature, not that it includes the index signature.
getValueForHey(new MyDataStructureClass());
// Generic parameter 'satisfies' constraint syntax proposal
declare function getValueForHey<T satisfies MyDataStructure>(myDataStructure: T): MyDataStructure["hey"];
Essentially, I'd like to type-check that my own interfaces and classes are providing key values which are safe within the format I am defining values for. If I define a key on the type and it has a value not supported by the format spec lined out in the index signature/primitives union type, then it should error. I want to ensure that only the properties I am defining myself can be accessed on the type, I don't want the index signature behavior in this case. I only want it to enable type checking against the interface values.
*Edit: An extension to my initial comment, it would be great to be able to do this with generics as well.
An alternative that could solve all of my changes mentioned above, would be if you could instead mark an index signature as use for type checking only. I think that may be a more elegant approach than the other ones here, as it essentially does exactly what I'm trying to do with the index signature, without having to change all of the places where index signatures could be used, just to enable the same functionality.
So, in a smaller example, this is my suggestion:
type JSONPrimitive =
| string
| number
| boolean
| JSONArray
| JSONObject
| null;
interface JSONArray<T extends JSONPrimitive = JSONPrimitive> extends Array<T> {}
// Neither of these should enable the standard index signature behavior, they should only validate that the types on a given shape you are validating have expected an acceptable value types.
// Alternative #1
interface JSONObject {
[key: string]: satisfies JSONPrimitive;
}
// Alternative #2
interface JSONObject {
satisfies [key: string]: JSONPrimitive;
}
I think it is kind of like const assertions on generic parameters instead of doing as const in calling code. It makes a lot of sense to say that T should satisfy and not should extend - especially for excess property checks. Right now there is no way to pass a generic parameter that extends objects union (non discriminate) with excess property checks without satisfies from outside.
@ecyrbe makes a great point in support of a type level satisfies
. It's fairly common to want to declare a type that's a subset of another while ensuring it remains compatible with the wider type.
Being able to quickly assert subtyping with satisfies
would have a dramatic effect on my time spent mucking with complex generic types. Throwing in an extends
means I don't have to dig through deeply nested type errors.
Could this also mitigate the need to create function declarations for compile-time asserts, i.e., declare function temp<T1>(): <T2 extends T1>(arg: T2) => T2;
? Both vitest and TSD seem to use this pattern.
Would it make sense to take this a step further and implement in/out variance annotations with the satisfies operator?
I think it is kind of like const assertions on generic parameters instead of doing as const in calling code. It makes a lot of sense to say that T should satisfy and not should extend - especially for excess property checks. Right now there is no way to pass a generic parameter that extends objects union (non discriminate) with excess property checks without satisfies from outside.
The use-case @Harpush describes is exactly what I'm facing now and led me to find this issue. Would be very happy if there was another way to do what I want but this is the challenge I'm facing:
type Foo = { a: number };
const foo = <T extends Foo>(x: T) => x;
foo({ a: 1, wrong: 2 }) // typechecks, but i don't want it to
foo({ a: 1, wrong: 2} satisfies Foo) // causes the type error i want
what I would really like to be able to write:
type Foo = { a: number };
const foo = <T satisfies Foo>(x: T) => x;
foo({ a: 1, wrong: 2 }) // causes the type error i want
Is this already possible in Typescript or would it need this feature to be implemented?
EDIT: Reading back the initial description of this issue, I am wondering whether what I'm describing is different enough to warrant a separate feature request, so I'll create one (and close it in favour of this one if it is considered a duplicate).
Adding on, this would help shimming utility types whose generics are not strict enough.
type FooBar = 'foo' | 'bar';
type Bar1 = Exclude<FooBar, 'baz'>; // will not error
type Bar2 = Exclude<FooBar, 'baz' satisfies FooBar>; // will error
Suggestion
satisfies
should work intype
declarations, similar to how it works currently.i.e. similar to
we could have
🔍 Search Terms
satisfies, type
✅ Viability Checklist
My suggestion meets these guidelines:
📃 Motivating Example
Say there is some library, 'to-upper', that deals with lower case letters, both Roman and Greek.
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,and then I use it with 'to-upper'
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: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 myLowercaseVowel
(which includes lowercase Greek characters) no longer satisfies the parameter oftoLower()
,LowercaseChar
, which doesn't.What would be preferable is if I had defined
LowecaseVowel
explicitly with the constraint that it satisfiesLowecaseChar
, i.e.(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.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.