-
-
Notifications
You must be signed in to change notification settings - Fork 478
My solutions #11
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
My solutions #11
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feedback on exercises 1–9, I'll leave more later!
@@ -0,0 +1,5 @@ | |||
{ | |||
"files.exclude": { | |||
"**/node_modules": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In exercise 10, it took me a bit to figure out that node_modules/str-utils/index.js
referred to exercises/exercise-10/node_modules
, not the top-level node_modules
. Adding this to my vscode settings made it easier to find.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to make a pull-request with this change?
@@ -64,8 +64,8 @@ const persons: Person[] = [ | |||
]; | |||
|
|||
function logPerson(person: Person) { | |||
let additionalInformation: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's interesting to note that this annotation is not needed. See this post for details.
@@ -95,9 +95,10 @@ function logPerson(person: Person) { | |||
console.log(` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`); | |||
} | |||
|
|||
function filterUsers(persons: Person[], criteria: User): User[] { | |||
type Criteria = Partial<Omit<User, 'type'>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the hint to use mapped types a bit confusing given that you don't need them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial
and Omit
are implemented using mapping. You can either use them directly or write a mapped type yourselves.
@@ -64,11 +64,18 @@ function logPerson(person: Person) { | |||
); | |||
} | |||
|
|||
function filterPersons(persons: Person[], personType: string, criteria: unknown): unknown[] { | |||
function getObjectKeys<T extends object>(o: T): (keyof T)[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instructions for this are:
Implement a function
getObjectKeys()
which returns proper type
for any argument given, so that you don't need to cast it.
"More convenient", sure, but "proper" is too strong here, string[]
really is the correct type for the reasons outlined in this StackOverflow answer and this TS issue.
There's also a discussion of this in Effective TypeScript (Item 54). I'm happy to get you a free copy if you'd like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to receive a copy if you can issue it to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to make a pull-request with this re-phrasing.
@@ -78,6 +85,11 @@ function filterPersons(persons: Person[], personType: string, criteria: unknown) | |||
let usersOfAge23: User[] = filterPersons(persons, 'user', { age: 23 }); | |||
let adminsOfAge23: Admin[] = filterPersons(persons, 'admin', { age: 23 }); | |||
|
|||
// @ts-ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a great use of @ts-expect-error
: filtering to admins shouldn't allow you to select by occupation.
// @ts-ignore | ||
let astronautAdmins: Admin[] = filterPersons(persons, 'admin', { occupation: 'Astronaut' }); | ||
|
||
let peopleOfAge23: Person[] = filterPersons(persons, Math.random() < 0.5 ? 'user' : 'admin', {age: 23}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call results in an error with the overloaded definitions, even though it is guaranteed to be valid. This is one of the problems with overloads, and it's a reason to prefer using conditional types. Rewriting this function to use conditional types instead of an overload is much harder but might be a good exercise for later.
You can learn more about why conditional types are preferred in this fantastic artsy post or Item 50 (Prefer Conditional Types to Overloaded Declarations) in Effective TypeScript.
@@ -41,7 +41,7 @@ interface Admin { | |||
role: string; | |||
} | |||
|
|||
type PowerUser = unknown; | |||
type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {type: 'powerUser'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't clear to me what you were supposed to do in this exercise if you didn't do the "higher difficulty" version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the normal difficulty version you manually write PowerUser
type without using neither User
and Admin
types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 The solution in the 'solutions' branch didn't work for me with TypeScript v3.9.7; I got multiple errors. The solution in this PR did work for me.
@@ -71,8 +71,16 @@ type ApiResponse<T> = ( | |||
} | |||
); | |||
|
|||
function promisify(arg: unknown): unknown { | |||
return null; | |||
function promisify<T>(fn: (callback: (arg: ApiResponse<T>) => void) => void): () => Promise<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a great problem (implement promisify
). The instructions could be more clear that you're expected to wrap errors in an Error
object.
Also unclear to me what work there is to do here if you don't do the "higher difficulty" challenge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also unclear to me what work there is to do here if you don't do the "higher difficulty" challenge.
This is just a higher difficulty exercise :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a great problem (implement promisify). The instructions could be more clear that you're expected to wrap errors in an Error object.
Feel free to provide a pull-request with this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that's all my feedback. Thanks again for the great exercises, I had a blast doing them. I especially loved the stories about CEO and his/her friend Nick. Let me know if there's anything I can do to help with this project!
function GetIndexFn<T>(input: T[], comparator: (a: T, b: T) => number): number; | ||
function GetElementFn<T>(input: T[], comparator: (a: T, b: T) => number): T | null; | ||
|
||
export const getMaxIndex: typeof GetIndexFn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's interesting that you can use typeof Fn
here to "inherit" the generic parameter. I don't think you can do such a thing with a type alias. (I tried above.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use type aliases like this
type MyFunction = <T>(arg: T): T;
@@ -1,13 +1,85 @@ | |||
export class Database<T> { | |||
import * as fs from 'fs'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exercise 13 was probably more work than all the others put together. I wonder if it could be split?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking of redoing 13 and later exercises to focus more on types.
}; | ||
|
||
type Unionize<T extends object> = {[k in keyof T]: {k: k, v: T[k]}}[keyof T]; | ||
type QueryableKeys<T extends object> = Extract<Unionize<T>, {v: JsonScalar}>['k']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enforces that you only run queries on fields whose values are JsonScalar
types. Maybe useful as an advanced extension checked with @ts-expect-error
.
// Can't use any of these operators with array types. | ||
expect( | ||
// @ts-ignore | ||
(await usersDatabase.find({grades: {$eq: 'Magical entity'}})).map(({_id}) => _id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since grades
is array-valued, this shouldn't be allowed. It was actually a little unclear to me from the problem statement if non-scalars were to be allowed in records, but they're valid JSON, so I didn't see why not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only scalars
are allowed in this implementation.
return words.every(w => r.$index[w]); | ||
} | ||
return Object.entries(q).every( | ||
([k, v]) => matchOp(v as FieldOp, r[k as keyof T] as any) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did use an any
here. Sorry! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make FieldOp
a generic, i.e. FieldOp<T>
and cast the other value to T
.
|
||
constructor(filename: string, fullTextSearchFieldNames) { | ||
constructor(filename: string, fullTextSearchFieldNames: (keyof T)[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to enforce that the fullTextSearchFieldNames
correspond to string
-valued properties in T
. One way to do this is with the Objectify
/ Unionize
generics I defined above (I'll be writing a blog post about these soon):
type StringKeys<T extends object> = Extract<Unionize<T>, {v: string}>['k'];
export class Database<T extends object> {
constructor(filename: string, fullTextSearchFieldNames: StringKeys<T>[]) {
// ...
}
}
It's pretty neat to see this working in autocomplete!
} | ||
|
||
async find(query, options?): Promise<T[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't clear to me why this was declared async
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid synchronous file operations. You read the database when necessary.
} | ||
|
||
async find(query, options?): Promise<T[]> { | ||
return []; | ||
async find(query: Query<T>, options?: Options<T>): Promise<Partial<T>[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a nice extension to have a more precise return type when options.projection
is and is not specified. (It should be Promise<T[]>
if it isn't and something less specific if it is.)
return rs; | ||
} | ||
|
||
async delete(query: Query<T>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the problem description, I was a little surprised that the tests didn't check that you wrote anything to disk. This problem felt like it had more to do with Node.js and less to do with TypeScript.
@@ -1,6 +1,6 @@ | |||
import {Database} from './database'; | |||
import * as path from 'path'; | |||
import {promises as fs} from 'mz/fs'; | |||
import {promises as fs} from 'fs'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got runtime errors about fs.copyFile
not being a function until I changed mz/fs
→ fs
.
Awesome~ |
Glad you liked it, thanks for the comments. |
VSCode settings for exercise #11
@@ -95,9 +95,10 @@ function logPerson(person: Person) { | |||
console.log(` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`); | |||
} | |||
|
|||
function filterUsers(persons: Person[], criteria: User): User[] { | |||
type Criteria = Partial<Omit<User, 'type'>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial
and Omit
are implemented using mapping. You can either use them directly or write a mapped type yourselves.
@@ -64,11 +64,18 @@ function logPerson(person: Person) { | |||
); | |||
} | |||
|
|||
function filterPersons(persons: Person[], personType: string, criteria: unknown): unknown[] { | |||
function getObjectKeys<T extends object>(o: T): (keyof T)[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to receive a copy if you can issue it to me.
@@ -64,11 +64,18 @@ function logPerson(person: Person) { | |||
); | |||
} | |||
|
|||
function filterPersons(persons: Person[], personType: string, criteria: unknown): unknown[] { | |||
function getObjectKeys<T extends object>(o: T): (keyof T)[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to make a pull-request with this re-phrasing.
@@ -41,7 +41,7 @@ interface Admin { | |||
role: string; | |||
} | |||
|
|||
type PowerUser = unknown; | |||
type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {type: 'powerUser'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the normal difficulty version you manually write PowerUser
type without using neither User
and Admin
types.
@@ -71,8 +71,16 @@ type ApiResponse<T> = ( | |||
} | |||
); | |||
|
|||
function promisify(arg: unknown): unknown { | |||
return null; | |||
function promisify<T>(fn: (callback: (arg: ApiResponse<T>) => void) => void): () => Promise<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also unclear to me what work there is to do here if you don't do the "higher difficulty" challenge.
This is just a higher difficulty exercise :)
|
||
1. DateDetails interface is missing | ||
time related fields such as hours, minutes and | ||
time-related fields such as hours, minutes and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to send me a pull-request with grammar fixes.
@@ -1,13 +1,85 @@ | |||
export class Database<T> { | |||
import * as fs from 'fs'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking of redoing 13 and later exercises to focus more on types.
return words.every(w => r.$index[w]); | ||
} | ||
return Object.entries(q).every( | ||
([k, v]) => matchOp(v as FieldOp, r[k as keyof T] as any) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make FieldOp
a generic, i.e. FieldOp<T>
and cast the other value to T
.
// Can't use any of these operators with array types. | ||
expect( | ||
// @ts-ignore | ||
(await usersDatabase.find({grades: {$eq: 'Magical entity'}})).map(({_id}) => _id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only scalars
are allowed in this implementation.
} | ||
|
||
async find(query, options?): Promise<T[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid synchronous file operations. You read the database when necessary.
…1-vscode VSCode settings for exercise typescript-exercises#11
export const getMaxElement: typeof GetElementFn; | ||
export const getMinElement: typeof GetElementFn; | ||
export const getMedianElement: typeof GetElementFn; | ||
export const getAverageValue: typeof GetElementFn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think getAverageValue
should be different from others
export function getAverageValue<T>(input: T[], getValue: (a: T) => number): number | null;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am agree,there different comparator in getAverageVal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think getAverageValue shouldn't be null
export function getAverageValue<T>(
input: T[],
getValue: (item: T) => any
): number;
}
{$or: Query<T>[]} | | ||
{$text: string} | | ||
( | ||
{[field in QueryableKeys<T>]?: FieldOp} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type can be improved by using distributive conditionals:
https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
This can be used to disallow {}
being passed as query. Ideally, it would also disallow passing multiple fields in one query, but TypeScript is not yet powerful enough to detect this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/fabb/typescript-exercises/blob/my_solutions/exercises/exercise-13/database.ts#L74
type Query<T> = FieldQuery<T> | AndQuery<T> | OrQuery<T> | TextQuery<T>
type FieldQuery<T, K extends keyof T = keyof T> = K extends string ? { [k in K]: Comparator<T[K]> } : never
First off, amazing work! I'd been thinking of creating homework / exercises / problem sets for Effective TypeScript, but given the work you've put into this I think I'll just be linking to yours instead.
Feedback on specific issues below, but some high level comments:
tsd
could help with assertions here, but the new@ts-expect-error
assertion (introduced in TS 3.9) might be your best bet. It produces an error unless the next line has a type error. So:will fail unless you pick a parameter type precise enough to exclude
ten
.Especially in the earlier exercises, I sometimes had a hard time figuring out what you were looking for in a solution until I saw which article you linked to as a hint. There are often many ways to do things in TypeScript (user-defined type guards,
in
tests, tagged unions) and some more guidance about which you were trying to teach might have been helpful.Unless I missed it, I don't think tagged unions are ever explicitly introduced. Exercise 3 has a
type
field but keeps the user-defined type guards (isAdmin
,isUser
) which are completely unnecessary when you have a tag. Given how pervasive tagged unions are as a construct in TS, it would be useful to have an exercise that forces the user to narrow a type based on its tag:instead of:
I'll leave more specific comments on each exercise.