-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Support satisfies
in type declaration
#52222
Comments
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 |
@RyanCavanaugh Without interface Broad {
property: string
}
interface NarrowA {
property: "foo"
}
//what if 'Broad' changes?
interface NarrowB {
propertyy: "bar"
//oops, typo
} With 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 ? 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 |
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;
} |
Added support for using generics types with the NBTData class, and it's surrounding functional APIs! Now if you know the shape of the NBT data going into the read function, you can pass that type into the read function generic parameter. This makes it so it can be a little easier to manage the types for the NBT structures going in and out of the library. This is better than just plain type assertions on top of the `NBTData.data` property, because now the `NBTData` class type itself can hold both the NBT file metadata itself, and the shape of the data that it holds. I'm planning on using this to describe the full NBT file data structure and file metadata for world save data entries, so then you know things like the endian format, compression type, and what the actual NBT in the file is. So `NBTData` can be a full representation of the entire NBT file and how to make it with NBTify! This will work great once I eventually need to make NBT-generating classes which can make defaults for the these files when necessary. The class will simply implement the file's `NBTData` interface shape (including the NBT structure, file metadata), then the class will be fully type-safe with all of the information needed to make a fully symmetrical data tree to that of what's generated by the vanilla game :) Currently, my use of validating the NBT data trees against the `CompoundTag` interface is currently typed too-strictly for what I'm trying to use it for, and it's forcing the interfaces and classes to require establishing index signatures, which are partially eliminating what I want the interface definitions to enable in the first place. I don't want my interfaces to allow the access and additions of unspecified properties, I only want to check that the values I am defining are compatible with the NBT primitive types. microsoft/TypeScript#52222 (comment) https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html I created a new issue (#28) under NBTify to track the development of this typing issue in terms of how NBTify is handling this problem.
Removing the API-safe `CompoundTag` type checking for the library entry points, in the meantime. Without the index signature key value checking behavior that I need to accomplish the value type checking, there isn't another way to create API designs that will make the compiler happy, unfortunately. So this means I'm going to make all objects acceptable into NBTify, on the type level at least. Since I can't use type checking to validate the types for the shapes I'm passing in, I will just have to allow them in, and make sure that the shapes have appropriate value types. While working on this revert, I looked into the `object` type, and realized it's kind of similar in a way as to what I'm trying to do with my primitive interface type checkers (`ListTag` and `CompoundTag`). > Introduction to TypeScript `object` type > The TypeScript `object` type represents all values that are not in primitive types. > > The following are primitive types in TypeScript: > > `number` > `bigint` > `string` > `boolean` > `null` > `undefined` > `symbol` For my object definitions, I'm trying to define an object with keys that are only of a select few types. For the `object` type, it represents all JavaScript types that aren't a primitive type. So with this change, NBTify simply only checks if your `CompoundTag` is none of the primitive values mentioned above, which is better than just using `any` at least. It would be better if the check could also specify that your `CompoundTag` object cannot have things like `Function`s, `symbol`s, `undefined`, `null`, things like that. I think that's my main reason for wanting to add parameter type checking for your `CompoundTag` use. https://www.typescripttutorial.net/typescript-tutorial/typescript-object-type/ microsoft/TypeScript#52222 (#28)
Added support for using generics types with the NBTData class, and it's surrounding functional APIs! Now if you know the shape of the NBT data going into the read function, you can pass that type into the read function generic parameter. This makes it so it can be a little easier to manage the types for the NBT structures going in and out of the library. This is better than just plain type assertions on top of the `NBTData.data` property, because now the `NBTData` class type itself can hold both the NBT file metadata itself, and the shape of the data that it holds. I'm planning on using this to describe the full NBT file data structure and file metadata for world save data entries, so then you know things like the endian format, compression type, and what the actual NBT in the file is. So `NBTData` can be a full representation of the entire NBT file and how to make it with NBTify! This will work great once I eventually need to make NBT-generating classes which can make defaults for the these files when necessary. The class will simply implement the file's `NBTData` interface shape (including the NBT structure, file metadata), then the class will be fully type-safe with all of the information needed to make a fully symmetrical data tree to that of what's generated by the vanilla game :) Currently, my use of validating the NBT data trees against the `CompoundTag` interface is currently typed too-strictly for what I'm trying to use it for, and it's forcing the interfaces and classes to require establishing index signatures, which are partially eliminating what I want the interface definitions to enable in the first place. I don't want my interfaces to allow the access and additions of unspecified properties, I only want to check that the values I am defining are compatible with the NBT primitive types. microsoft/TypeScript#52222 (comment) https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html I created a new issue (#28) under NBTify to track the development of this typing issue in terms of how NBTify is handling this problem.
Removing the API-safe `CompoundTag` type checking for the library entry points, in the meantime. Without the index signature key value checking behavior that I need to accomplish the value type checking, there isn't another way to create API designs that will make the compiler happy, unfortunately. So this means I'm going to make all objects acceptable into NBTify, on the type level at least. Since I can't use type checking to validate the types for the shapes I'm passing in, I will just have to allow them in, and make sure that the shapes have appropriate value types. While working on this revert, I looked into the `object` type, and realized it's kind of similar in a way as to what I'm trying to do with my primitive interface type checkers (`ListTag` and `CompoundTag`). > Introduction to TypeScript `object` type > The TypeScript `object` type represents all values that are not in primitive types. > > The following are primitive types in TypeScript: > > `number` > `bigint` > `string` > `boolean` > `null` > `undefined` > `symbol` For my object definitions, I'm trying to define an object with keys that are only of a select few types. For the `object` type, it represents all JavaScript types that aren't a primitive type. So with this change, NBTify simply only checks if your `CompoundTag` is none of the primitive values mentioned above, which is better than just using `any` at least. It would be better if the check could also specify that your `CompoundTag` object cannot have things like `Function`s, `symbol`s, `undefined`, `null`, things like that. I think that's my main reason for wanting to add parameter type checking for your `CompoundTag` use. https://www.typescripttutorial.net/typescript-tutorial/typescript-object-type/ microsoft/TypeScript#52222 (#28)
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 |
Being able to quickly assert subtyping with Could this also mitigate the need to create function declarations for compile-time asserts, i.e., Would it make sense to take this a step further and implement in/out variance annotations with the satisfies operator? |
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 [email protected].
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.The text was updated successfully, but these errors were encountered: