Description
This is going to sound a bit crazy, but hear me out. There are currently a number of issues with this proposal, but it exists solely for the sake of demonstration. We can probably get around the limitations.
I have been experimenting with making the query function strong-typed, so that queries are validated at compile-time.
Features
- Validates that type names are correct in the query.
- Validates that the projection contains valid property names.
- Validates the criteria section (
where
), including.- Checking for valid property names.
- Checking for valid operators for particular types.
string
only allows:=
andis
.!=
andis_not
.in
andnot_in
.like
andnot_like
.
number
only allows:=
andis
.!=
andis_not
.in
andnot_in
.>
,after
andgreater_than
.<
,before
andless_than
.>=
and<=
.
boolean
only allows:=
andis
.!=
andis_not
.in
andnot_in
.
object
only allows:has
.any
.
- Checking for valid
where
values.- If the property name does not exist, it gives a compile error.
- If for instance the property name being queried is a string, it must be wrapped in quotes.
- If the property name being queried is a number, it must be numeric.
Current limitations
If you like where this is headed, I am willing to invest in trying to resolve these limitations. So please don't see them as direct show-stoppers. Let's have a discussion about it.
- I'd like this to work with
infer
, so that instead of writingstrongTypedQuery<Foo>('select fooProperty1 from Foo')
, it can just be inferred fromstrongTypedQuery('select fooProperty1 from Foo')
, sinceFoo
is already a valid type name. This would be awesome, and would automatically add types for all queries, especially in combination with the type generator project. - Right now,
has
andany
is not being validated. Can easily add this. - Right now, the
Whitespace
string literal only accepts a single whitespace. So an arbitrary amount of whitespaces between for instance the property names is not allowed. This could be a problem with multiline etc. And adding more might impact the type checking performance. and
andor
is not supported in thewhere
clause. This is easy to add, but only with a specific maximum amount of potential properties being selected (due to TypeScript not supporting recursive string literal types). There may be ways to work around this.- The projection part can only contain one property. This is easy to fix, but only with a specific maximum amount of potential properties being selected (due to TypeScript not supporting recursive string literal types). There may be ways to work around this.
- Right now, date types are not supported, but again very easy to add and even validate.
The code
Here's the code. Notice the strongTypedQuery
function. That's where the magic happens. For every call to that, I've commented particular cases that the strong-typings catch.
type Foo = {
fooProperty1: string,
fooProperty2: number,
fooProperty3: boolean
}
type Bar = {
barProperty1: string,
barProperty2: number,
barProperty3: boolean
}
interface EntityTypeMap {
Foo: Foo,
Bar: Bar
}
type EntityTypes = EntityTypeMap[keyof EntityTypeMap];
type EntityTypeProperty<TEntityType extends EntityTypes, TPropertyType> = {
[K in keyof TEntityType]: TEntityType[K] extends symbol ?
never :
TEntityType[K] extends TPropertyType ?
K :
never
}[keyof TEntityType];
type EntityType<TEntityType extends EntityTypes> = keyof EntityTypeMap;
type Whitespace = ` `;
type OptionalWhitespace = Whitespace | "";
type Projections<TEntityType extends EntityTypes, TPropertyType> =
`${EntityTypeProperty<TEntityType, TPropertyType>}`;
type ProjectionPrefix<TEntityType extends EntityTypes> =
`select${Whitespace}${Projections<TEntityType>}${Whitespace}from${Whitespace}` |
"";
type CriteriaOperator<T> =
T extends string ?
(
`${'='|'is'}` |
`${'!='|'is_not'}` |
`${'in'}` |
`${'not_in'}` |
`${'like'}` |
`${'not_like'}`
) :
T extends number ?
(
`${'='|'is'}` |
`${'!='|'is_not'}` |
`${'in'}` |
`${'not_in'}` |
`${'>'|'after'|'greater_than'}` |
`${'<'|'before'|'less_than'}` |
`${'>='}` |
`${'<='}`
) :
T extends boolean ?
(
`${'='|'is'}` |
`${'!='|'is_not'}` |
`${'in'}` |
`${'not_in'}`
) :
T extends Object ?
(
`${'has'}` |
`${'any'}`
) :
never;
type CriteriaValue<T> =
T extends number ?
`${number}` :
T extends boolean ?
`${boolean}` :
T extends string ?
`'${string}'` :
never;
type CriteriaAndValue<TEntityType, TPropertyType> =
`${EntityTypeProperty<TEntityType, TPropertyType>}${Whitespace}${CriteriaOperator<TPropertyType>}${Whitespace}${CriteriaValue<TPropertyType>}`
type Criteria<TEntityType extends EntityTypes> = {
[K in keyof TEntityType]:
`${CriteriaAndValue<TEntityType, TEntityType[K]>}`
}[keyof TEntityType];
type CriteriaSuffix<TEntityType extends EntityTypes> =
`${Whitespace}where${Whitespace}${Criteria<TEntityType>}` |
``;
type Query<TEntityType extends EntityTypes> =
`${ProjectionPrefix<TEntityType>}${EntityType<TEntityType>}${CriteriaSuffix<TEntityType>}`
function strongTypedQuery<TEntityType extends EntityTypes>(query: Query<TEntityType>) {
return null!;
}
//valid, because "Foo" is a known type.
strongTypedQuery<Foo>("Foo");
//gives compile error, because "Blah" is not a valid type.
strongTypedQuery<Blah>("Blah");
//valid, because fooProperty1 exists on Foo
strongTypedQuery<Foo>("select fooProperty1 from Foo");
//gives compile error, because fooProperty1 does not exist on bar.
strongTypedQuery<Bar>("select fooProperty1 from Bar");
//valid, because fooProperty1 exists on Foo, and fooProperty1 is of type string.
strongTypedQuery<Foo>("Foo where fooProperty1 = 'abc'");
//gives compile error, because fooProperty1 is of type string, not number, so the value has to be wrapped in quotes.
strongTypedQuery<Foo>("Foo where fooProperty1 = 2388");
//gives compile error, because fooProperty1 is of type string, not boolean.
strongTypedQuery<Foo>("Foo where fooProperty1 = true");
//valid, because fooProperty2 is a number.
strongTypedQuery<Foo>("Foo where fooProperty2 = 24");
//gives compile error, because fooProperty2 is a number, not a string, so the value can't be wrapped in quotes.
strongTypedQuery<Foo>("Foo where fooProperty2 = 'abc'");
//gives compile error, because fooProperty2 is a number, not a boolean.
strongTypedQuery<Foo>("Foo where fooProperty2 = true");
//valid, because fooProperty3 is of type boolean.
strongTypedQuery<Foo>("Foo where fooProperty3 = false");
//gives compile error, because fooProperty3 is a boolean, so quotes are not allowed.
strongTypedQuery<Foo>("Foo where fooProperty3 = 'abc'");
//gives compile error, because fooProperty3 is a boolean, so numbers are not allowed.
strongTypedQuery<Foo>("Foo where fooProperty3 = 28");
//valid, because fooProperty1 exists on Foo, and fooProperty1 is of type string.
strongTypedQuery<Foo>("Foo where fooProperty1 = 'abc'");
//gives compile error, because fooProperty1 is a string, so the "<=" operator is not allowed.
strongTypedQuery<Foo>("Foo where fooProperty1 <= 'abc'");
//gives compile error, because fooProperty1 is a string, so the "has" operator is not allowed.
strongTypedQuery<Foo>("Foo where fooProperty1 <= 'abc'");
//long query syntax can also be used.
strongTypedQuery<Foo>("select fooProperty1 from Foo where fooProperty1 = 'abc'");
Playground link
Play around with it here. Right now it gives an error in line 90, but it doesn't seem to be a valid error. I think it actually might be a bug in TypeScript.