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
Make function types generic in type definitions #3168
Conversation
Impressive! What makes it WIP? |
@kibertoad Thanks for taking a look so quickly. I have outlined on the motivation of my change in #3156 and would like to have better tests after receiving some feedback (hence the WIP). |
@lorefnon Thank you very much for this commit, I did some tests and it's so nice to have types in query results. However, I found a bug in type inference for const results = await knex('Users').select(['id', 'isMaster']) // results has wrong type any[] & Pick<any, "id" | "isMaster">
results.isMaster // bug: this property 'isMaster' exists
results[1].qwerty // bug: elements of array have <any> type I expect |
@pvider Thanks for taking a look. This PR is still early stage and will take some time to stabilize. I have now added tests for most common crud scenarios. The autocompletion for single-table operations should be much better now: Auto completion of column name in where: Type safety in column values (number type inferred for id attribute): Type safety in update argument: |
@pvider @kibertoad I believe this is now adequately tested. Please let me know if you have any comments. |
@lorefnon I did some tests in VSC through pure JS with JSDOC and should note it works really well. My test results are same as yours. This is really powerful addition to knex even for pure-JS-users like me, so I want to share the following snippet for those who don't need TypeScript yet, but wants same power via JSDOC (works from the box in VSC): /**
* @typedef {Object} User
* @property {number} id
* @property {number} age
* @property {string} name
* @property {boolean} active
* @returns {import('knex-generic-type-defs').QueryBuilder<User, {}>}
*/
const Users = () => knex('Users') The above typedef is analogue of this TypeScript interface: interface User {
id: number;
age: number;
name: string;
active: boolean;
} Next we define returned type via // infered type:
// Pick<User, "id">[]
const result = await Users().select('id') |
@pvider Thanks for trying this out. If this PR gets merged, we can submit another to add your example to the docs - I am sure it will benefit many people. |
Thanks a lot! |
Thanks a lot for investing time into making the TS integration even better! Where can I find docs for this and how would I migrate from |
I'm using Knex in TypeScript and till now enforced return types via Documentation on how to use this would be greatly appreciated. The few examples I found in the discussions about the linked issues and pull requests (like the example of |
@lorefnon thank you for this PR. I'm on the latest TS (3.4.5) What it can be? I've wasted 4 hours today to figure out how to make it work, without any success. |
created an issue for this the above stuff: #3193 |
@loxs Is 0.16.7 not working for you either? From my understanding it was supposed to be using |
@kibertoad It's not working for me and I still can't figure out why. Had to revert the update to latest, will probably give it another go tomorrow. |
Ok, the impact of this change was much more severe than I originally anticipated. I apologize for the wasted time and effort here - this could certainly have been managed better. @kibertoad Let me know if you'd like me to send a PR that reverts the changes. Unpublishing generally results in a poor experience for people (esp. corporations) who use npm proxies because afaik popular solutions like verdaccio don't handle unpublishing well and people continue to receive unpublished code. So the safest thing to do here might be to republish another minor version after reverting the changes. I can continue iterating on the ideas here in a branch (or a personal fork) and submit a PR once I have a better understanding of all usage patterns (which, as has now been proven, I don't at the moment). @felixmosh This particular example will not work because of So the general idea is that inference works as long as the things being selected are keys of the Record type. But the moment we find usage like However the columns function is generic, so it could be provided with the type of the result:
Once you specify a result type it would be retained down the fluent chain. @loxs This is interesting usage. If you could share some snippets of your usage, I would be happy to check on this further. Given io-ts uses very similar inference based approach to what I was using in this PR, it should be possible to extract static types from your io-ts runtime types and use them in the generic parameters to Knex functions. |
@lorefnon yeah, I'll try to give a nice example when I have some time. I am also willing to participate in testing with new pull requests etc. Please, keep me in the loop. |
@lorefnon Valid point about proxies, but I still would prefer not to revert the change, but iterate over it and also create some documentation instead. In my experience success rate of long-running branches or forks for developing complex features is really low, and I would expect feature just to die off if we follow that route. I believe that lack of proper type safety is one of major drawbacks of knex as it is now, so it is well worth the gamble. Would be awesome if we all could come together and iterate over a series of -next release to make sure that transition is as painless and covers as much ground as possible. |
Ok alright. Sounds good to me. |
0.17.0-next is now out. Suggestions, problem reproductions and PRs most welcome! |
0.17.0-next2 is out and should be more of a drop-in replacement. @loxs @Xiphe @felixmosh Can you give it a try and see if you are having any problems with it? |
I'll work on adding some documentation here, but in the meanwhile this test file can be used as a reference for patterns which are known to be type safe and APIs which are now generic (to aid in intellisense). Also, as @kibertoad mentioned, we don't expect any existing usage to break compilation (the result can of course be While I am still in process of parallely expanding the tests to cover all usage patterns, given the large surface area of this library (and me not having indepth familiarity with the source) some aspects may get missed and I'll be happy to look closer into any snippets which fail to compile. |
Hi, next2 is better for sure, as probably half of the problems are gone. Here is an attempt to explain my use case and question on how would one go to make it compile. I start building a query in one function, then pass the intermediate to another, then to another. Logic branches at some points and finally I execute it with await. Till now I only annotated my functions like const addWhereClause = (intermediateQuery: Knex, more, args): Knex => {
return knex.where("of course with some logic")
} I finally do something like this (decoding with io-ts): const sqlRes = await query;
const result = decodeSqlResultSet(sqlRes, MyIOTSCodec); When I try to compile with new Knex I get errors of this kind:
I kind of get where is this coming from, but probably will need a lot of effort to try and make it work. Hopefully it won't require rewriting all of my query generators.
This one I don't get at all. These two errors are prevalent across my whole codebase |
OK, if I put a bunch of Though this seems a bit worse than before, as previously I had this, which no more works, though I might have been wrong before. (expected by export type TGenericResultSet = Array<{ [key: string]: any }>;
// and later
const result : TGenericResultSet = await query Probably the modern equivalent would be something like this (which still doesn't work): const query: QueryBuilder<any, TGenericResultSet> = dbConn.select() // etc
const result = await query This one fails with:
|
@felixmosh @loxs New iteration of typings is out: knex@0.17.0-next3 |
I will try it tomorrow, thanks! |
@felixmosh Thanks for pointing out these issues.
These aspects will work as expected now.
I wasn't able to reproduce this. Are you using typescript 3.2+ ?
Actually, there hasn't been any change here. the |
@loxs Thanks for your comments. I have however, not been able to fully comprehend the aspects pointed out in #3168 (comment).
I believe this will work if you use I tried this in previous version of Knex as well, and received an error there as well (I am assuming strict mode usage):
I am assuming that this is a snippet extracted from a larger usage, and in the actual code something was a conversion (to any, or a cast) happening somewhere.
This does work (though is not type-safe) if query has not been casted to anything else before.
This will not work. So taking a step back, one of the changes introduced here is that the Knex instance internally tracks the result type. By default the result type is But the caveat of this is that intermediate However, all the functions where result type can change, accept a generic type parameter for directly modifying the result type. So we can write the above as:
And thanks to inference we can get rid of the duplication:
Which is exactly the same thing. You can also assign this to
If the above doesn't address your problem adequately, it would be great if you could share your typescript version, tsconfig.json and some samples which are still failing. |
@lorefnon - yes, my code snippets above were wrong (I typed them in the GitHub interface directly). And yes, what you say makes sense and I'll play some more with my code and see if all goes well (I think it should). I'll probably need to refactor my code a lot, but that's probably a good thing, as it will be more type safe. We'll also see how it plays with |
@kibertoad you have (probably by mistake) published it as |
Good catch. Will fix asap, sorry |
@loxs I'd like to prevent the need for unnecessary refactoring. Even if that may need some more iterations in the definitions here. Also, I would like to clarify that these changes don't significantly improve type safety in most practical situations (when we have aliases and joins), though they make the experience of working in type aware editors better. There is nothing stopping the library consumer from passing the wrong result type as a generic param and Knex will trust it completely. For true slick-style typesafety we would need to derive the types from the database itself and provide type-safe helpers for aliasing, but we are not quite there yet. As a part of a different project I have been working on database driven type generation but it is not in as good a state that I can extract it out and publish it as something more generic. So yeah, I think retaining your io-ts based validations is a good idea esp. if you use encoders for datetime etc. Your examples in #3168 (comment) and #3168 (comment) arent working because the Because of the |
I can confirm that I still get the error that is related to ERROR in /Users/xxx/Projects/node/xxx/api/node_modules/knex/types/index.d.ts(1328,13):
TS2430: Interface 'ChainableInterface<T>' incorrectly extends interface 'Bluebird<T>'.
Types of property 'asCallback' are incompatible.
Type '(callback: Function) => this' is not assignable to type '{ (callback: (err: any, value?: T) => void, options?: SpreadOption): this; (...sink: any[]): this; }'.
Type 'this' is not assignable to type 'this'. Two different types with this name exist, but they are unrelated.
Type 'ChainableInterface<T>' is not assignable to type 'this'. There are still errors related to const response = await db(translationsTable)
.select('key', 'value')
.where({ namespace: query.namespace })
.reduce((result, { key, value }) => {
result[key] = value;
return result;
}, {});
Error:(48, 30) TS2684: The 'this' context of type 'WhereResult<any, (DeferredKeySelection<any, string, true, {}, boolean> | DeferredKeySelection<any, "value" | "key", true, {}, false>)[]>' is not assignable to method's 'this' of type 'Bluebird<any[] & Iterable<any>>'.
Types of property 'nodeify' are incompatible.
Type '{ (callback: (err: any, value?: any[]) => void, options?: SpreadOption): this; (...sink: any[]): this; }' is not assignable to type '{ (callback: (err: any, value?: any[] & Iterable<any>) => void, options?: SpreadOption): Bluebird<any[] & Iterable<any>>; (...sink: any[]): Bluebird<any[] & Iterable<any>>; }'.
Type 'this' is not assignable to type 'Bluebird<any[] & Iterable<any>>'.
Type 'Bluebird<R>' is not assignable to type 'Bluebird<any[] & Iterable<any>>'.
Type 'R' is not assignable to type 'any[] & Iterable<any>'.
Type 'R' is not assignable to type 'any[]'. |
@felixmosh Can you also please provide me outputs of |
@lorefnon Thanks for the explanations. It makes more sense now. Though I still fail to make it compile and the best thing I have so far is this: Here is the code itself if you want to play with it: import * as t from 'io-ts';
import {QueryBuilder} from 'knex';
type TGenericResultSet = Array<{ [key: string]: any }>;
const CCountries = t.type({
id: t.number,
identifier: t.string,
name: t.string,
currency_id: t.number,
currency_identifier: t.string,
currency_from_base: t.string,
currency_to_base: t.string
});
type TCountries = t.TypeOf<typeof CCountries>;
export const decodeIOObject = <T extends {}>(
obj: { [key: string]: any },
codec: t.TypeC<T>
): t.TypeOf<typeof codec> => {
const decoded = codec.decode(obj);
if (decoded.isRight()) {
return decoded.value;
} else {
console.log('Offending object:', obj);
const errPath = PathReporter.report(decoded);
errPath.forEach(e => console.error(e));
throw new Error('Unexpected input object');
}
};
export const decodeSqlResultSet = <T extends {}>(
objs: TGenericResultSet,
codec: t.TypeC<T>
): Array<t.TypeOf<typeof codec>> => {
return objs.map((r: any) => {
return decodeIOObject(r, codec);
});
};
export const getCountries = async (
dbConn: QueryBuilder,
countryIds: number[]
): Promise<Country[]> => {
const q1 = dbConn
.select<TGenericResultSet>([
'countries.id',
'countries.identifier as identifier',
'countries.name',
'countries.currency_id',
'currencies.identifier as currency_identifier',
'latest_currency_rates.from_base as currency_from_base',
'latest_currency_rates.to_base as currency_to_base'
])
.from('countries')
.join('currencies', 'countries.currency_id', '=', 'currencies.id')
.join('latest_currency_rates', 'currencies.id', '=', 'latest_currency_rates.currency_id')
.orderBy('name');
const q2 = countryIds.length ? q1.whereIn('countries.id', countryIds) : q1;
const sqlRes = await q2;
const result = decodeSqlResultSet(sqlRes, CCountries);
return countryIds.length ? mapResultSetToIds(countryIds, result) : result;
}; |
@felixmosh @loxs New version is out, hopefully fixing what was reported so far: knex@0.17.0-next4 |
This example will work now. You'd want to use arrays as result type, ie. |
WOW! it just works! KODUS for the hard work |
Awesome! If no new issues are reported next week, I'll release a new stable version. |
I can confirm that my software compiles and passes tests now. |
Thank you for testing, everyone! Final version of 0.17.0 is out. |
No description provided.