-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Enforcing types to be narrowed from generic types #26332
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
Comments
You can sort of do this with mapped types: type MustBeStringUnion<T extends string> = string extends T ? never : T;
declare function f<T extends string>(input: MustBeStringUnion<T>): void;
f('some random string'); // Error
type U = "a" | "b";
declare const u: U;
f(u); // Error?
f<U>(u); // OK The conditional type seems to break inference though, so you'd need an explicit type annotation. |
I might be missing something, but I cannot get that to work with an array 🤔 type MustBeStringUnion<T extends string> = string extends T ? never : T
interface EnumSchema<T extends string> {
enum: MustBeStringUnion<T>[]
}
declare function foobar<T extends string>(t: EnumSchema<T>): boolean
foobar({ enum: ['a' as 'a', 'b' as 'b'] }) But yeah, unfortunately, my main concern is for the consumers of is-my-json-valid not to have to type Maybe this is the wrong approach though 🤔 The interesting thing is that I can sort of get it to work on the So possibly this could be solved instead with variadic kinds instead (#5453) 🤔 Something like this: interface EnumSchema<...E> {
enum: [...E]
}
declare function foobar<...E>(schema: EnumSchema<...E>): boolean
foobar({ enum: [1, 2] }) Which would basically select the variant: @Andy-MS if you feel like it, I would love some quick eyes on these definitions, it seems like I might be missing some smart trick to fix the nested require types 🤔 ❤️ https://github.com/mafintosh/is-my-json-valid/blob/master/index.d.ts |
Yeah, when the conditional type is involved you don't seem to get any type inference, and have to provide a type argument with |
Looking at the type definitions, it seems it would be a lot simpler if you inferred the schema from the type rather than the other way around: export = createValidator
declare function createValidator<T>(schema: createValidator.SchemaFor<T>, options?: any): createValidator.Validator<T>
declare namespace createValidator {
type Validator<T> = ((input: unknown) => input is T) & { errors: ValidationError[] }
type SchemaFor<T> =
T extends null ? { required?: boolean, type: "null" }
: T extends boolean ? { required?: boolean, type: "boolean" }
: T extends number ? { required?: boolean, type: "number" }
: T extends string ? { required?: boolean, type: "string" }
: T extends Array<infer U>
? { type: "array", items: SchemaFor<U> }
: ObjectSchemaFor<T>
interface ObjectSchemaFor<T> {
type: "object"
properties: { [K in keyof T]: SchemaFor<T[K]> }
required?: boolean
additionalProperties?: boolean
}
interface ValidationError {
field: string
message: string
value: unknown
type: string
}
function filter<T>(schema: SchemaFor<T>, options?: any): Filter<T>
type Filter<T> = (t: T) => T
} Then the example from the README will compile: import validator = require('is-my-json-valid')
interface I {
hello: string;
}
var validate: validator.Validator<I> = validator({
required: true,
type: 'object',
properties: {
hello: {
required: true,
type: 'string'
}
}
});
var filter: validator.Filter<I> = validator.filter({
required: true,
type: 'object',
properties: {
hello: {type: 'string', required: true}
},
additionalProperties: false
})
var doc = {hello: 'world', notInSchema: true}
console.log(filter(doc)) // {hello: 'world'} |
The goal with the package is to be able to infer the TypeScript interface from the JSON Schema, such as to avoid having to type it twice. I wasn't able to get TypeScript to infer even the simplest schema when going the other way around: declare type SchemaFor<T> =
T extends null ? { type: "null" }
: T extends boolean ? { type: "boolean" }
: T extends number ? { type: "number" }
: T extends string ? { type: "string" }
: never
declare function createValidator<T>(schema: SchemaFor<T>): any
// This errors with: Argument of type '{ type: string; }' is not assignable to parameter of type 'never'.
createValidator({ type: 'string' }) One huge thing when writing an API is dealing with input validation, and when using TypeScript I almost always end up typing everything twice; once in JSON Schema, and then in TypeScript as well in order to get proper typings. This is very tedious and also leads to hard to spot mistakes, e.g. I small mismatch between the JSON Schema and the TypeScript interface can make me think that everything is working, but when I deploy the API it will actually reject the input I intended. I really appreciate you taking the time to respond to me 💌 |
It looks like you can get contextual types on string literals (and not have to write type AnySchema = NullSchema | BooleanSchema | NumberSchema | StringSchema | AnyArraySchema | AnyObjectSchema
interface NullSchema { type: 'null' }
interface BooleanSchema { type: 'boolean' }
interface NumberSchema { type: 'number' }
interface StringSchema { type: 'string' }
interface ArraySchema<ItemSchema extends AnySchema> { type: 'array', item: ItemSchema }
interface AnyArraySchema extends ArraySchema<AnySchema> {}
interface ObjectSchema<PropertiesSchemas extends Record<string, AnySchema>> { type: 'object', properties: PropertiesSchemas }
interface AnyObjectSchema extends ObjectSchema<Record<string, AnySchema>> {}
type TypeFromSchema<S extends AnySchema> =
S extends NullSchema ? null
: S extends BooleanSchema ? boolean
: S extends NumberSchema ? number
: S extends StringSchema ? string
: S extends ArraySchema<infer ItemSchema> ? ArrayFromSchema<ItemSchema>
: S extends ObjectSchema<infer PropertySchemas> ? { [K in keyof PropertySchemas]: TypeFromSchema<PropertySchemas[K]> }
: never
interface ArrayFromSchema<S extends AnySchema> extends Array<TypeFromSchema<S>> {}
declare function createValidator<S extends AnySchema>(schema: S): TypeFromSchema<S> // Return TypeFromSchema<S> to make it easy to test
const sArr = createValidator({ type: 'array', item: { type: 'string' } })
const s: string = sArr[0]
const o = createValidator({ type: 'object', properties: { a: { type: 'boolean' } } })
const b: boolean = o.a |
Wow, this is amazing 😍 Looks great 👏 Now I only need to get the Thank you so much, this is awesome ❤️ Actually, I've been here before 😂 The problem now is that when we add the --- a.ts 2018-08-09 20:56:58.000000000 +0100
+++ b.ts 2018-08-09 20:57:52.000000000 +0100
@@ -5,8 +5,8 @@
interface StringSchema { type: 'string' }
interface ArraySchema<ItemSchema extends AnySchema> { type: 'array', item: ItemSchema }
interface AnyArraySchema extends ArraySchema<AnySchema> {}
-interface ObjectSchema<PropertiesSchemas extends Record<string, AnySchema>> { type: 'object', properties: PropertiesSchemas }
-interface AnyObjectSchema extends ObjectSchema<Record<string, AnySchema>> {}
+interface ObjectSchema<PropertiesSchemas extends Record<string, AnySchema>, RequiredFields extends keyof PropertiesSchemas> { type: 'object', properties: PropertiesSchemas, required?: RequiredFields[] }
+interface AnyObjectSchema extends ObjectSchema<Record<string, AnySchema>, any> {}
type TypeFromSchema<S> =
S extends NullSchema ? null
@@ -14,7 +14,7 @@
: S extends NumberSchema ? number
: S extends StringSchema ? string
: S extends ArraySchema<infer ItemSchema> ? ArrayFromSchema<ItemSchema>
- : S extends ObjectSchema<infer PropertySchemas> ? { [K in keyof PropertySchemas]: TypeFromSchema<PropertySchemas[K]> }
+ : S extends ObjectSchema<infer PropertySchemas, infer RequiredFields> ? { [K in keyof PropertySchemas]: (K extends RequiredFields ? TypeFromSchema<PropertySchemas[K]> : (TypeFromSchema<PropertySchemas[K]> | undefined)) }
: never
interface ArrayFromSchema<S> extends Array<TypeFromSchema<S>> {} The failure will then be: const input = null as unknown
const personValidator = createValidator({
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: [
'name'
]
})
if (personValidator(input)) {
assertType<string>(input.name)
assertType<number | undefined>(input.age)
} Which can be fixed by: required: [
- 'name'
+ 'name' as 'name
] |
Oh, and we can fix this by adding an overload specifically for an object schema, where the object schema is the root: +declare function createValidator<PropertiesSchemas extends Record<string, AnySchema>, RequiredFields extends keyof PropertiesSchemas>(schema: ObjectSchema<PropertiesSchemas, RequiredFields>): TypeFromSchema<ObjectSchema<PropertiesSchemas, RequiredFields>> // Return TypeFromSchema<S> to make it easy to test
declare function createValidator<S extends AnySchema>(schema: S): TypeFromSchema<S> // Return TypeFromSchema<S> to make it easy to test But that leads us to the next problem: const input = null as unknown
const user2Validator = createValidator({
type: 'object',
properties: {
name: {
type: 'object',
properties: {
first: { type: 'string' },
last: { type: 'string' },
},
required: [
'last' // <----- `as 'last'` is required here. ------
]
},
items: {
type: 'array',
items: { type: 'string' },
}
},
required: [
'name'
]
})
if (user2Validator(input)) {
// --------
// ...otherwise `input.name` will be `never` here, since string doesn't extend keyof {first: ..., last: ...}
// --------
assertType<{ first: string | undefined, last: string }>(input.name)
assertType<string | undefined>(input.name.first)
assertType<string>(input.name.last)
if (input.items !== undefined) {
assertType<number>(input.items.length)
assertType<string>(input.items[0])
}
} and here I'm at a loss 🤔 |
The problem with declare function f0<K extends string>(i: I<K>): K;
const a0 = f0({ k: "a" }); // "a"
declare function f1<K extends string, T extends I<K>>(i: T): F<T>;
const a1 = f1({ k: "a" }); // "a"
type F<T> = T extends I<infer K> ? K : never;
declare function f2<T extends I<string>>(i: T): F<T>;
const a2 = f2({ k: "a" }); // string
// Existential type:
declare function f3<T extends I<?>>(i: T): F<T>;
const a3 = f2({ k: "a" }); // "a" That would also allow you to express relations between types, such as that |
Ahh, yeah that makes sense 🤔 I'm having a bit of a hard time grasping exactly how existential types works, even after reading the other thread twice 😆 But from what I gathered it would allow me to express this:
Which is awesome! I'll guess I'll have to go with a ton of overloads for now, but it would be really really nice to see existential types, and variadic kinds get into TypeScript Still, I'm not sure that that would fix my initial problem with the interface EnumSchema<T> { enum: T[] }
declare function createValidator<T>(schema: EnumSchema<T>): T[]
// This would give `number[]` instead of `(1 | 2 | 3)[]`
createValidator({ enum: [1, 2, 3] })
// This would work though
createValidator({ enum: [1 as 1, 2 as 2, 3 as 3] }) I don't have a perfect proposal for how this could be solved though 🤔 Maybe just a interface EnumSchema<narrow T> { enum: T[] }
declare function createValidator<narrow T>(schema: EnumSchema<T>): T[] That would disallow the generic types |
If you want to avoid writing code for a schema, maybe you could go with a VSCode extension? I haven't used it yet, but at first glance that looks kinda like what you want to do here. |
Seems very cool! Although my express goal right now is to provide typings for the package |
This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow. |
TLDR; You probably can make use of the new See this question and my answer: https://stackoverflow.com/questions/37978528/typescript-type-string-is-not-assignable-to-type/55387357 |
Search Terms
enforce narrowing
,narrow types
,narrow
,force narrow type
Suggestion
First of all, sorry if there is already a way to do this, I've been reading thru the documentation, searched thru the issues, and crawled the web in order to find anything, but haven't so far.
I would like a way to force narrow types to be passed to my function, and specifically disallow the generic versions (e.g.
string
,number
,boolean
).What I want is for my function to accept any subtype of e.g.
string
, but notstring
itself. So both'a' | 'b'
,'a'
, and'hello' | 'world' | '!'
would be accepted, but notstring
.Use Cases
I think that this is one of the most illustrating use cases:
mafintosh/is-my-json-valid#165
Basically, I want to have a type that maps a JSON Schema into a TypeScript type. I then provide a function with a return value of
input is TheGeneratedType
.Currently, the enum values need to be typed with
'test' as 'test'
in order not to be generalized as juststring
:Examples
Related
#16896
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: