I have the following generic interface in my typescript code:
interface BaseResponse<T> {
status_code: string;
data: T;
}
I thought I would be able to use that base interface, without specifying the base's type parameter, in a generic function like this:
class MyService {
static async post<T extends BaseResponse>(path: string, data: any): Promise<T> {
// implementation here
}
}
But this gives the following error:
Generic type 'BaseResponse<T>' requires 1 type argument(s).(2314)
I can fix this error by updating the code like so:
class MyService {
static async post<T extends BaseResponse<U>, U>(path: string, data: any): Promise<T> {
// implementation here
}
}
But this requires me to pass two type parameters when I call the function as below. I was hoping I could only pass one and it could infer the second, but that gives me the error Expected 2 type arguments, but got 1.(2558). Is there any way to accomplish this?
// What I want to be able to do (Causes error mentioned above):
const response1 = await MyService.post<CustomerResponse>('/customers', postData);
// What I have to do instead (note the two type parameters)
const response2 = await MyService.post<CustomerResponse, CustomerData>('/customers', postData);
Related
I have the following code on typescript playground and a few questions come up that I am not sure how to get working
class PathInfo {
functionName: string;
httpPath: string;
httpMethod: string;
constructor(functionName: string, httpPath: string, httpMethod: string) {
this.functionName = functionName;
this.httpPath = httpPath;
this.httpMethod = httpMethod;
}
toString(): string {
return "PathInfo["+this.functionName+","+this.httpPath+","+this.httpMethod+"]";
}
}
class AuthRequest {}
class AuthResponse {}
class LoginRequest {}
class LoginResponse {}
const path: any = (thePath: string, type: any) => {
return (target: Function, memberName: string, propertyDescriptor: PropertyDescriptor) => {
const pathMeta = new PathInfo(memberName, path, type);
Object.defineProperty(target, memberName+'pathInfo', {
value: pathMeta,
writable: false
});
//How do I access the stored pathMeta
//console.log("target="+target.pathInfo);
console.log("member="+memberName);
console.log("props="+propertyDescriptor);
}
}
class AuthApiImpl {
#path("/authenticate", AuthResponse)
authenticate(request: AuthRequest): Promise<AuthResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
#path("/login", LoginResponse)
login(request: LoginRequest): Promise<LoginResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
};
function printMethods(obj: any) {
console.log("starting to print methods");
for (var id in obj) {
console.log("id="+id);
try {
//How do I access the stored pathMeta here FOR EACH METHOD ->
//console.log("target="+target.pathInfo);
if (typeof(obj[id]) == "function") {
console.log(id+":"+obj[id].toString());
}
} catch (err) {
console.log(id + ": inaccessible"+err);
}
}
}
console.log("starting to run")
const temp = new AuthApiImpl();
printMethods(temp);
console.log("done")
line 64-65, how to read the property that I set
line 40-41, how to read the property that I set
line 58-74, why is this not printing any functions? I want to print all functions and I do NOT want to print properties (just functions)
line 33, Can I access the class name at this point?
line 35, I thought target was a function and would be authorize, then login, BUT if I define the property as JUST 'pathInfo', I get an error that the property is already defined on the target(This implies the target is the class not the function?). I am so confused.
Terribly sorry as I try to focus on a single question, but this one test of writing decorators has given me more questions than answers as I delve into the typescript world.
How can I tweak the code to play more here?
A goal here is as developers define the APIs of other microservices, I can capture a bunch of meta information and store it SOMEWHERE I can use later in startup code. I do not care where I store that really, but just need a clean way of knowing the class I want to extend, the methods, the return types, the http path, etc.
How to get methods of a class
You still can't grab the method names even if you remove the decorator. This isn't a TypeScript specific question.
You need to get the properties of the prototype, not just the object itself.
function printMethods(obj: any) {
console.log("starting to print methods");
const objProto = Object.getPrototypeOf(obj);
console.log(Object.getOwnPropertyNames(objProto));
}
How to access class names
Don't think this is possible with decorators at the moment, but it should be straightforward to just pass in your class name as a string.
Similar issue: TypeScript class decorator get class name
Open issue on GitHub: https://github.com/microsoft/TypeScript/issues/1579
"property is already defined on the target"
Notice if you run the code above you get the following in console.log:
["constructor", "authenticate", "login", "authenticatepathInfo", "loginpathInfo"]
I also want to point out that if you don't even initialize an instance of the class, you'll still get the same error.
I want to read this meta data in nodejs and use that to dynamically create a client implementing the api. Basically, developers never have to write clients and only write the api and the implementation is generated for them.
If I were to do that, I'd probably not use decorators, but mapped types:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function generateClient<S extends object>(apiInfo: ApiInfo<S>): Client<S> {
const client = {} as Client<S>;
for (const key in apiInfo) {
const info = apiInfo[key as keyof S];
client[key] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
interface MyServer {
authenticate(request: AuthRequest): AuthResponse;
login(request: LoginRequest): LoginResponse;
}
const myApiInfo: ApiInfo<MyServer> = { // compiler verifies that all methods of MyServer are described here
authenticate: {
httpPath: '/authenticate',
httpMethod: 'POST'
},
login: {
httpPath: '/login',
httpMethod: 'POST'
}
}
const myClient = generateClient(myApiInfo); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
(To understand how these type definitions work, you may want to read the section Creating Types from Types in the TypeScript Handbook)
The weakness of this approach is that while the compiler can derive the client signatures from the server signatures, it will not copy any JSDoc, so client devs can not easily access the API documentation.
In the above code, I chose to specify the metadata in a separate object rather than decorators so the compiler can check exhaustiveness (decorators are always optional; the compiler can not be instructed to require their presence), and because decorators are an experimental language feature that may still change in future releases of the language.
It is entirely possible to populate such a metadata object using decorators if that's what you prefer. Here's what that would look like:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
const apiMethodInfo = Symbol("apiMethodInfo");
function api(info: ApiMethodInfo) {
return function (target: any, propertyKey: string) {
target[apiMethodInfo] = target[apiMethodInfo] || {};
target[apiMethodInfo][propertyKey] = info;
}
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function invokeApi(info: ApiMethodInfo, param: any) {
console.log(info, param);
}
function generateClient<S extends object>(serverClass: new() => S): Client<S> {
const infos = serverClass.prototype[apiMethodInfo]; // a decorator's target is the constructor function's prototype
const client = {} as Client<S>;
for (const key in infos) { // won't encounter apiMethodInfo because Symbol properties are not enumerable
const info = infos[key];
client[key as keyof S] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
class MyServer {
#api({
httpPath: '/authenticate',
httpMethod: 'POST'
})
authenticate(request: AuthRequest): AuthResponse {
throw new Error("Not implemented yet");
}
#api({
httpPath: '/login',
httpMethod: 'POST'
})
login(request: LoginRequest): LoginResponse {
throw new Error("Not implemented yet");
}
}
const myClient = generateClient(MyServer); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
Notice how using a Symbol prevents name collisions, and ensures that other code doesn't see this property (unless they look for that particular Symbol), and therefore can not be tripped up by its unexpected presence.
Also notice how MyServer, at runtime, contains the constructor of the class, whose prototype holds the declared instance methods, and it being passed as target to any decorators thereof.
General Advice
May I conclude with some advice for the recovering Java programmer? ;-)
EcmaScript is not Java. While the syntax may look similar, EcmaScript has many useful features Java does not, which often allow writing far less code. For instance, if you need a DTO, it is wholly unnecessary to declare a class with a constructor manually copying each parameter into a property. You can simply declare an interface instead, and create the object using an object literal. I recommend looking through the Modern JavaScript Tutorial to familiarize yourself with these useful language features.
Also, some features behave differently in EcmaScript. In particular, the distinction between class and interface is quite different: Classes are for inheriting methods from a prototype, interfaces for passing data around. It's quite nonsensical to declare a class for a Response that will be deserialized from JSON, because prototypes don't survive serialization.
I'm struggling to make the fields of my request DTOs case insensitive.
export class ExampleDto {
dateOfBirth?: string
}
Now I want to accept
{ "dateofbirth": "19880101" }
{ "dateOfBirth": "19880101" }
{ "DATEOFBIRTH": "19880101" }
My first thought was to implement a middleware which just looks at the incoming body and "extends it" with lower & upper case mappings for all incoming fields.
But that doesn't meet my requirements due to camel case, which I definitely want to keep as the default.
Any ideas on how to do this?
You could create a custom Pipe where you try the different options and finally return the Dto instance:
export class CaseInsensitiveExampleDtoPipe implements PipeTransform{
transform(body: any, metadata: ArgumentMetadata): ExampleDto {
const dto = new ExampleDto();
dto.dateOfBirth = body.dateOfBirth || body.dateofbirth || body.DATEOFBIRTH;
return dto;
}
In your controller you can then use it as follows:
#UsePipes(new CaseInsensitiveExampleDtoPipe())
async postNewExample(#Body() exampleDto: ExampleDto) {
// ...
}
Since JavaScript properties start existing after their initialization, you cannot "see" the definition of dateOfBirth?: string and therefor you won't be able to match it against the received JSON.
A possible solution for that is to enforce the creation of the properties of all of your DTO's with a constructor:
export class ExampleDto {
dateOfBirth: string
constructor(dateOfBirth: string){
this.dateOfBirth = dateOfBirth;
}
}
Then, you'll be able to iterate over the ExampleDto's properties and match them with a pipe (the received type can be derived from metadata):
#Injectable()
export class IgnoreCasePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const dto = new metadata.metatype;
const dtoKeys = Object.getOwnPropertyNames(dto);
Object.keys(value).forEach(key => {
const realKey = dtoKeys.find(dtoKey => dtoKey.toLocaleLowerCase() === key.toLocaleLowerCase());
if (realKey) {
dto[realKey] = value[key];
}
});
return dto;
}
}
Either inject it globally in main.ts or wherever it's needed - just bear in mind that you'll need to create a constructor for each DTO.
Note: this would work for a single-level class. If you want to support something like people: PersonDto[] in your classes then you'll need to recursively find all of the nested keys and match them - something like this.
I have this object model:
export interface UpdateDocument {
updated_at?: string;
actions?: Actions;
}
export interface Actions {
update?: Update;
create?: Create;
}
export interface Update {
name?: Values;
priority?: Values;
engine?: Values;
fact?: Fact;
}
export interface Fact {
brand?: Values;
model?: Values;
version?: Values;
year?: Values;
km?: Values;
color?: Values;
}
export interface Values {
old?: any;
new?: any;
}
export interface Create {
conditions?: Object;
recipe?: Object;
}
In this function i tried to pass a parameter to references an objects field and do an assignment:
async buildUpdateDocument (updateDocument: UpdateDocument) {
let fields: Array<string> = ['name','priority','engine','fact','adjustment'];
fields.forEach((field: string) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}
but i hav this ts-error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Update'.
No index signature with a parameter of type 'string' was found on type 'Update'.ts(7053)
How can i pass the parameter in this kind of reference to do the assignment?
First of you have specified a wrong key adjustment that doesn't exist on Update. This example uses a explicit type (as const):
let fields = ['name','priority','engine','fact'] as const;
Make sure to not add a type definition to the variable when using as const.
Here is the modified function to better fit TS standards. This also addresses the forEach-async problem in the original code. The real correct structure would be null checks for each of the x | undefined types, but to override the type errors the following is the way to go.
async function buildUpdateDocument (updateDocument: UpdateDocument) {
const fields: Array<keyof Update> = ['name','priority','engine','fact'];
await Promise.all(fields.map(async (field) => {
(((updateDocument.actions as {update: Update}).update)[field] as Values).old = await this.getValue(field);
}));
}
Your current code has bugs that the type system would help you find if you let it. First, the adjustment field doesn't exist on the Update type, and old field doesn't exist on the Fact type.
To implement this properly, I would use a Record for the data type instead:
const updateFields = ['name', 'priority', 'engine', 'fact'] as const
export type UpdateFields = typeof updateFields[number]
export type Update = Record<UpdateFields, Values>
And then, your function will look like this:
async buildUpdateDocument (updateDocument: UpdateDocument) {
updateFields.forEach((field) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}
I'm currently trying to find an abstraction that can allow me to run Firebase products (mainly Firestore, Storage, and Analytics) regardless of the platform (React Native, React, Node.js). I have looked at the REST API but would like to use the SDKs for all the features that they offer.
// web
import firebase from 'firebase';
type WebFirestore = ReturnType<typeof firebase.firestore>;
// cloud
import * as admin from 'firebase-admin';
type CloudFirestore = ReturnType<typeof admin.firestore>;
// native
import { FirebaseFirestoreTypes } from '#react-native-firebase/firestore';
type NativeFirestore = FirebaseFirestoreTypes.Module;
const API = (firestore: WebFirestore | CloudFirestore | NativeFirestore) => {
firestore
.collection('foo')
.doc('foo')
.get()
.then((resp) => true);
}
I'm trying to create a TypeScript type that can enable me to do the same (at least that's what I think). The API, on the outset, is kept consistent across platforms for these products but my guess is that the return types are different. By that I mean, I can run this function on all platforms as long as the firestore object belongs to the SDK on that platform.
I was thinking of creating a class that takes a flag ('web', 'cloud', 'native') and then also take the firestore object in the constructor. I tried running the code below but TypeScript says the following:
(property) Promise<T>.then: (<TResult1 = FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>, TResult2 = never>(onfulfilled?: (value: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = firebase.firestore.DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: firebase.firestore.DocumentSnapshot<...>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = FirebaseFirestoreTypes.DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: FirebaseFirestoreTypes.DocumentSnapshot<...>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>)
Attaches callbacks for the resolution and/or rejection of the Promise.
#param onfulfilled — The callback to execute when the Promise is resolved.
#param onrejected — The callback to execute when the Promise is rejected.
#returns — A Promise for the completion of which ever callback is executed.
This expression is not callable.
Each member of the union type '(<TResult1 = DocumentSnapshot<DocumentData>, TResult2 = never>(onfulfilled?: (value: DocumentSnapshot<DocumentData>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: DocumentSnapsh...' has signatures, but none of those signatures are compatible with each other.ts(2349)
I'm rather new to TypeScript and was wondering if there is a way to make this work. All the types individually work but their union doesn't work. Is there a better way to think about this layer of abstraction in TypeScript? I intend to host this on the Github package registry and all the products to have access to the internal API as functions that are currently - firestore, cloud storage, cloud functions, some REST API calls.
Switching based on string flag is almost never the "right" way. You want to replace if conditions with a level of abstraction.
Adapter Pattern
You might want to read up on the Adapter Pattern, which is a generalized OOP approach to this sort of situation. Instead of one class with type property, you would have a separate wrapper class for each type of store instance. These classes would all have the same public API interface SharedFirestore, but internally they could call different methods on their this.firestore to get the results. When you want to use a firestore, you would just require the type SharedFirestore and you would know that you could interact with it the same regardless of which store type it is.
That sort of setup looks like:
interface SharedFirestore {
getDoc( collectionPath: string, documentPath: string ): Document;
}
class WebFirestore implements SharedFirestore {
private firestore: firebase.firestore.Firestore;
constructor( app?: firebase.app.App ) {
this.firestore = firebase.firestore(app);
}
getDoc( collectionPath: string, documentPath: string ): Document {
return this.firestore.collection(collectionPath).doc(documentPath);
}
}
class CloudFirestore implements SharedFirestore {
private firestore: FirebaseFirestore.Firestore;
constructor( app?: admin.app.App ) {
this.firestore = admin.firestore(app);
}
getDoc( collectionPath: string, documentPath: string ): Document {
return this.firestore.someOtherMethod( collectionPath, documentPath );
}
}
Typescript Generics
Wrapper classes are not necessary here because the three types already implement the same interface, kind of. They all allow you to get a document by calling firestore.collection(collectionPath).doc(documentPath).get(). This is purely a typescript issue which is caused by the differing return types.
web.collection('foo').doc('foo').get();
// type: firebase.firestore.DocumentSnapshot<firebase.firestore.DocumentData>
cloud.collection('foo').doc('foo').get();
// type: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
native.collection('foo').doc('foo').get();
// type: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
Your then callback is a function of the document, but you don't know which of the three types of document you have. So you cannot call a function on the union. Instead we need to say "whichever type of store I have, my callback will match that". To do that, we make the API a generic which depends on the store type.
We can use some conditional types to extract the associated types for the collection and the document from the store type.
interface BaseCollection<D> {
doc(path: string): D;
}
interface BaseStore<C extends BaseCollection<any>> {
collection(path: string): C;
}
type CollectionFromStore<S> = S extends BaseStore<infer C> ? C : never;
type DocFromCollection<C> = C extends BaseCollection<infer D> ? D : never;
type DocFromStore<S> = DocFromCollection<CollectionFromStore<S>>
Here's a possible setup that uses generics to extend a base type rather than extending a union.
class FirebaseAPI <S extends BaseStore<any>> {
constructor( private firebase: S ) {}
getCollection( collectionPath: string ): CollectionFromStore<S> {
return this.firebase.collection(collectionPath);
}
getDoc( collectionPath: string, documentPath: string ): DocFromStore<S> {
return this.getCollection(collectionPath).doc(documentPath);
}
}
You can see how we get the appropriate return types.
(new FirebaseAPI(web)).getDoc('', '').get().then(v => {});
// v has type firebase.firestore.DocumentSnapshot<firebase.firestore.DocumentData>
(new FirebaseAPI(cloud)).getDoc('', '').get().then(v => {});
// v has type FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
(new FirebaseAPI(native)).getDoc('', '').get().then(v => {});
// v has type FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
Playground Link
I have this code for deferring the execution of a function
export type DeferredFunction<T> = () => T | PromiseLike<T>;
export class Deferrable<T> {
protected df: DeferredFunction<T>;
constructor(df: DeferredFunction<T>) {
this.df = df;
}
public async execute(): Promise<T> {
return this.df();
}
}
export const defer = <T>(df: DeferredFunction<T>): Deferrable<T> => new Deferrable<T>(df);
That works fine and I can run code like
await defer(() => someFunction('foo', 'bar')).execute();
but I what I want to do is type DeferredFunction in a way that I can specify the inner function's signature but I can't get it working. In generic cases the above works but when I want to limit the arguments such that they are specific to a certain type of function I don't have that kind of control.
For clarity, I want to be able to type the inner function's inputs like (as an example)
export type InnerDeferredFunction<T> = (a: string, b: number, c: SomeObjectType) => T | PromiseLike<T>
Any help would be greatly appreciated!
What "inner function" are you talking about? Is it someFunction? If so then the type of DeferredFunction<T> has no handle on it, since it's a function called by the implementation of DeferredFunction<T>. There's no way in TypeScript to specify "a function whose implementation must call a function of type (x: string, y: number, z: boolean) => string". Implementation details are not part of a function's call signature.
The only way I can imagine to begin to approach this would be for DeferredFunction<T> to accept as a parameter the inner function you want to call, along with the list of arguments to call it with. This might not be what you're looking for, but it's the closest that the type system can represent.
Something like this:
export type InnerDeferredFunction<T, A extends any[]> = (...args: A) => T | PromiseLike<T>;
export type ZeroArgDeferredFunction<T> = InnerDeferredFunction<T, []>
Here I'm keeping A generic but you can specify it to some hardcoded list of arguments. I've renamed your DeferredFunction to ZeroArgDeferredFunction to be explicit that it doesn't need arguments.
But now Deferrable needs to know about T and A:
export class Deferrable<T, A extends any[]> {
protected df: ZeroArgDeferredFunction<T>;
constructor(df: InnerDeferredFunction<T, A>, ...args: A) {
this.df = () => df(...args);
}
public async execute(): Promise<T> {
return this.df();
}
}
And you can see that you have to construct one by passing it the inner function and its arguments, and the ZeroArgDeferredFunction is built inside the constructor and is not passed in.
There are different ways to define defer(). It could be a thin wrapper around new Deferrable the way you had it, or you could imagine splitting it up so that the args come first:
export const defer = <A extends any[]>(...args: A) => <T>(
df: InnerDeferredFunction<T, A>): Deferrable<T, A> => new Deferrable<T, A>(df, ...args);
And then you can test it like this:
function someFunction(x: string, y: string) {
return (x + y).length;
}
function anotherFunction(x: number, y: number) {
return (x * y).toFixed()
}
const deferFooBar = defer('foo', 'bar');
await deferFooBar(someFunction).execute(); // okay
await deferFooBar(anotherFunction); // error! string is not assignable to number
Once you call deferFooBar('foo', 'bar'), the returned value will only accept functions that can be safely called with the arguments foo and 'bar'. That means someFunction will be accepted and anotherFunction will be rejected.
Okay, hope that helps; good luck!
Playground link to code