Using typescript generics properly - javascript

I have an interface of the following format which describes database methods like so:
export default interface IRepository {
createAndSave(data: ICreateUserDTO): Promise<User>
findById<T>({ id }: { id: number }): Promise<T | null> // right here
...
}
As you can see from the snippet above, findById method is meant to take in a type and return a resolved promise of type T or a null value. I go-ahead to implement this in a class like so.
class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t
}
...
}
When I try to create the findById method like that, typescript gives this error of this format
Type 'import("../src/user/infra/typeorm/entities/User").default' is not assignable to type 'User'.
'User' could be instantiated with an arbitrary type which could be unrelated to 'import("../src/audience/infra/typeorm/entities/User").default'
I tried to use typescript assertion to override this error like so
class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t as Promise<User> // here
}
...
}
but I still get the error, I am not really sure what to do from this point onward.
Here is what the User model definition looks like, I am making use of TypeORM
export default class User {
#PrimaryGeneratedColumn('uuid')
id: string
#Column({
type: 'json',
nullable: true,
})
data: object
#Column({ type: 'tinyint', default: 1 })
status: number
...
}
What could be the cause of this and how do I rectify it? Any help will be appreciated. Thank you very much!

The IRepository.findById method's type signature doesn't mean what you think it means.
When you write findById<T>, it means that the method promises to work with any type T. Whoever calls the method chooses which type it is. Kind of like this:
const r : IRepository = ...
const x = r.findById<User>( ... )
const y = r.findById<number>( ... )
consy z = r.findById<string>( ... )
... and so on
And since the caller of the method can choose any type, it means that the implementer of the method must implement it such that it can work with any type. So it can't be just User. It has to be any type, whatever the caller happens to choose.
Now, what you probably meant to do was to create not just a repository, but a repository of a certain thing. To do this, the generic parameter should be on the interface, not on the method:
export default interface IRepository<T, DTO> {
createAndSave(data: DTO): Promise<T>
findById({ id }: { id: number }): Promise<T | null>
...
}
Then you can implement IRepository<User, ICreateUserDTO> in your class:
class UserRepository {
...
async createAndSave(data: ICreateUserDTO): Promise<User> {
...
}
async findById({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t as Promise<User> // here
}
...
}

Related

Using Uint8Array as an Argument for mutation in typegraphql

I need to save a public key to my postgres database from graphql. Using a Uint8Array seems the best option to save this with (let me know if you have a better suggestion). Therefore I have the entity:
#ObjectType()
#Entity()
export class Wallet extends BaseEntity {
#Field()
#PrimaryGeneratedColumn()
id!: number;
#Field()
#Column({ type: 'bytea' })
public_key!: Uint8Array;
}
And the resolver:
#Mutation(() => User)
async createUser(
#Arg("email") email: string,
#Arg("wallets", _type => [Uint8Array], { nullable: true }) wallets: Uint8Array[],
#Ctx() { em }: MyContext
): Promise<User> {
// input validation
const user = {
email: email,
wallets: (wallets ? wallets.map(e => ({ 'public_key': e })) : [])
}
return { user: await em.save(User, user) };
}
CannotDetermineGraphQLTypeError: Cannot determine GraphQL input type for argument named 'wallets' of 'createUser' of 'UserResolver' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper input value?
I'm not sure I understand this error properly, can I not use Uint8Array as an input to my mutation or am I doing it incorrectly?
GraphQL does not support binary types out of the box. It is recommended to use Base-64 encoded strings instead, which works well for small payloads. Read more: Binary in GraphQL

TS: Infer literal typing based on chained methods

There is only one thing left for me to unblock my flow and publish the first trial version of my form validation lib.
I have the following code (of course I'm omitting a lot of things so it doesn't get too big)
interface Validation {
name: string
message: string
params?: Record<string, any>
test: (value: any, params?: any) => boolean
}
class MixedSchema<T> {
type!: T
validations: Validation[] = []
required(message?: string) {
this.validations.push({
name: 'required',
message: message,
test: (value: any) => {
return value === '' ? false : true
},
})
return this
}
oneOf(arrayOfValues: any[], message?: string) {
type Params = { arrayOfValues: any[] }
this.validations.push({
name: 'oneOf',
params: { arrayOfValues },
message: message,
test: (value: any, params: Params) => {
return params.arrayOfValues.includes(value)
},
})
return this
}
}
class StringSchema extends MixedSchema<string> {
email() {
// this.validations.push({ ... })
return this
}
maxWords(maxWords: number, message?: string) {
// this.validations.push({ ... })
return this
}
}
class NumberSchema extends MixedSchema<number> {
min(min: number, message?: string) {
// this.validations.push({ ... })
return this
}
round(type: 'toUp' | 'toDown' | 'closer') {
// this.validations.push({ ... })
return this
}
}
const schema = {
string() {
return new StringSchema()
},
number() {
return new NumberSchema()
},
// array, file, etc..
}
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
type InferType<T> = {
[P in keyof T]: T[P] extends MixedSchema<infer TS> ? TS : never
}
type Form = InferType<typeof form>
So I get the following result
But I need to get the most real typing possible, I mean, similar to the schema defined for form, example
interface HowNeedsToBe {
email: string
age: number | undefined
gender: 'male' | 'female'
color: 'red' | 'blue' | 'green' | undefined
}
I believe that the logic is something like, in the absence of the required, puts an undefined and if there is oneOf, substitutes the argument with the T of the MixedSchema<T>, but I don’t know how to send this back to up to MixedSchema<T>, actually I think this logic is a mess.
I already researched about map and generics in typescript, but I confess that when it comes to putting it into practice, nothing good comes out.
Here's the TS playground if you want to try.
Conceptually you want the required() and oneOf() methods to narrow T; this would be easy enough to give types to (although you need type assertions to avoid compiler errors, since the compiler can't verify that you have actually done the requisite narrowing). So required(), called on a MixedSchema<T>, should return a MixedSchema<Exclude<T, undefined>> (using the Exclude utility type to remove undefined from any union members of T). And oneOf() should be generic in the element type U of the elements of arrayOfValues, and should return a MixedSchema<U | Extract<undefined, T>> (using the Extract utility type to keep undefined if the T can be undefined).
Here's how it might look (implementations elided for brevity):
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): MixedSchema<Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
MixedSchema<U | Extract<undefined, T>>
}
Unfortunately the fact that you are subclassing MixedSchema is complicating matters; you want to say that, for example, a StringSchema should stay a StringSchema of some kind after calling required(); it should not be widened back to MixedSchema:
declare class StringSchema<T extends string | undefined> extends MixedSchema<T> {
email(): this;
maxWords(maxWords: number, message?: string): this;
}
declare class NumberSchema<T extends number | undefined> extends MixedSchema<T> {
min(min: number, message?: string): this;
round(type: 'toUp' | 'toDown' | 'closer'): this;
}
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // MixedSchema<string>
t.email(); // error! Property 'email' does not exist on type 'MixedSchema<string>';
So we will need something more complicated.
The "right" answer here is to use so-called higher-kinded types of the sort requested (but not implemented) in microsoft/TypeScript#1213. You'd like to say something like: MixedSchema<T>'s required() method should return this<Exclude<T, undefined>>, where you are somehow treating this like a type that takes a generic parameter. So if this is a StringSchema<T>, then it should be StringSchema<Exclude<T, undefined>>. But there's no direct support for this.
Instead we need to simulate it, and all such simulations will involve some amount of "registering" the types we'd like to be able to treat like a generic-of-a-generic:
type Specify<C extends MixedSchema<any>, T> =
C extends NumberSchema<any> ? NumberSchema<Extract<T, number | undefined>> :
C extends StringSchema<any> ? StringSchema<Extract<T, string | undefined>> :
MixedSchema<T>;
We've listed out all the subclasses of MixedSchema that we care about and described how to specify their type parameters. So while we can't write this<Exclude<T, undefined>>, but we can write Specify<this, Exclude<T, undefined>> and have the same effect.
Here's the new implementation of MixedSchema:
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): Specify<this, Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
Specify<this, U | Extract<undefined, T>>
}
And we can verify that it now behaves appropriately in subclasses:
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // StringSchema<string>
t.email(); // okay
Let's make sure that the types are inferred as you'd like:
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
/* const form: {
email: StringSchema<string>;
age: NumberSchema<number | undefined>;
gender: StringSchema<"male" | "female">;
color: StringSchema<"red" | "blue" | "green" | undefined>;
} */
That's a good sign; the generic type parameter for each field has been specified the right way. And thus your InferType should be able to grab those fields types:
type Form = InferType<typeof form>
/* type Form = {
email: string;
age: number | undefined;
gender: "male" | "female";
color: "red" | "blue" | "green" | undefined;
} */
Looks good!
Playground link to code

TypeScript: Getting the type signature of values in a dynamically keyed object

I'd like to reference the value type in a type object I'm importing from a library and do not control.
I am using the serverless TypeScript package and it provides a type called AWS that looks like this:
{
...
functions?: {
[k: string]: {
name: string;
...
}
}
}
I know can reference the type of the entire functions sub-object with AWS["functions"]
My question is, how can I programmatically ask for the sub-type of the values of that sub-object?
See the code below with "TYPE_HERE" indicating where I want to fill in the type.
import type { AWS } from "#serverless/typescript";
export default (): TYPE_HERE => ({
name: "",
...
});
How can I state the type using the root type, or even the type AWS["functions"], when it exists as the value property of a dynamic key?
This should work:
interface AWS {
functions?: {
[k: string]: {
name: string;
}
}
}
type FnType
= NonNullable<AWS["functions"]> extends { [K: string]: infer R } ? R : never
const d = (): FnType => ({
name: "",
dd: '', // error
});
TS playground

Reuse generic types of extended interface when creating a shorthand type

I'm trying to create a shorthand type for my actions that work with AsyncActionCreators objects.
I've created a simple function which takes a React dispatch: Dispatch<T> parameter:
const fetchProfileAction = actionCreator.async<void, Profile, any>('FETCH_PROFILE');
// AsyncActionCreators<Params = void, Result = Profile, Error = any>;
type AsyncDispatch<A extends AsyncActionCreators<Params, Result, Error>, Params, Result, Error> = Dispatch<
ReturnType<A['started'] | A['done'] | A['failed']>
>;
export const fetchProfile = (
dispatch: AsyncDispatch<typeof fetchProfileAction, void, Profile, any>,
) => async () => {
dispatch(fetchProfileAction.started());
try {
dispatch(fetchProfileAction.done({ result: (await api.get<Profile>('/profile')).data }));
} catch (e) {
dispatch(fetchProfileAction.failed(e));
}
};
The problem for now is that I have to specify manually the types used within fetchProfileAction:
AsyncDispatch<typeof fetchProfileAction, void, Profile, any>
Is it possible to simplify that and end up with something like AsyncDispatch<typeof fetchProfileAction> with autimatically resolved Params, Result, Error types of AsyncActionCreators?
You could use type inference in conditional types:
type AsyncDispatch<A> = A extends AsyncActionCreators<infer Params, infer Result, infer Error>
? Dispatch<ReturnType<A['started'] | A['done'] | A['failed']>>
: never;

Generic type for ngrx Actions in Angular 7

I have dynamic action:
export class ChangeCurrentConfig implements Action {
readonly type = ActionTypes.ChangeCurrentConfig;
constructor(public payload: { key: string, value: any }) {}
}
Where key is dynamic key of the reducer. For example i have:
interface Config1 {
myPath1: string
}
interface Config2 {
myPath2: string
}
And i load them dynamically in CurrentConfig reducer. And in effects of every config i handle changes for every config. For example Config1 effect:
#Effect()
changeMyPath1$ = this.actions$.pipe(
ofType(ChangeMyPath1),
map((action: ChangeMyPath1) => action.payload.myPath1),
map(value => {
return new ChangeCurrentActivityConfig({
key: 'myPath1',
value,
});
})
);
The problem is that there is easy to make a mistake in this line key: 'myPath1',. I want to prevent this with generic for ChangeCurrentConfig action. I tried:
export class ChangeCurrentConfig<T> implements Action {
readonly type = ActionTypes.ChangeCurrentConfig;
constructor(public payload: { key: keyof T, value: any }) {}
}
It works good, but i want to make this generic type required for new instances of the class. So if i write in effects new ChangeCurrentActivityConfig without generic type it will show an error. How i can achieve this?

Categories

Resources