TypeScript Compiler API: Get resolved return type of a generic method - typescript-compiler-api

Supposing we have a generic class or an interface:
interface A<T> {
prop1: T;
func2(): T;
}
interface B extends A<C> {
}
interface C {
}
We need to get the return type of the B.func2 method (not T but C).
The way described here works OK for props, but I can't figure out how to modify it for methods:
for (const statement of sourceFile.statements) {
if (!isInterface(statement))
continue;
if (!statement.heritageClauses?.length)
continue;
for (const heritageClause of statement.heritageClauses) {
for (const exprWithTypeArgs of heritageClause.types) {
const baseType = checker.getTypeAtLocation(exprWithTypeArgs);
for (const propSymbol of baseType.getProperties()) {
const resolvedType = checker.getTypeOfSymbolAtLocation(propSymbol, exprWithTypeArgs);
console.log(`${propSymbol.name} has type: ${resolvedType.symbol?.name}`);
// prints
// prop1 has type: C
// func2 has type: func1
for (const propDeclaration of propSymbol.declarations) {
if (!isSignatureDeclaration(propDeclaration))
continue;
const signature = checker.getSignatureFromDeclaration(propDeclaration);
const returnTypeSymbol = checker.getReturnTypeOfSignature(signature)?.symbol;
const resolvedReturnType = checker.getTypeOfSymbolAtLocation(returnTypeSymbol, exprWithTypeArgs);
console.log(`${propSymbol.name} return type: ${resolvedReturnType.symbol?.name}`);
// prints
// func2 return type: undefined
}
}
}
}
}
What is the correct way of getting resolved return type of a method?

The TypeChecker#getSignaturesOfType method allows for getting the signature of a type.
const bDecl = sourceFile.statements[1]; // obviously, improve this
const bType = typeChecker.getTypeAtLocation(bDecl);
const func2Symbol = bType.getProperty("func2")!;
const func2Type = typeChecker.getTypeOfSymbolAtLocation(func2Symbol, func2Symbol.valueDeclaration);
const func2Signature = checker.getSignaturesOfType(func2Type, ts.SignatureKind.Call)[0];
checker.typeToString(func2Signature.getReturnType()); // C

Related

Typescript transformer, `node.parent` is undefined

I'm currently using a typescript transformer api, and I found that the node.parent is undefined.
My code is:
const transformerFactory: ts.TransformerFactory<ts.Node> = (
context: ts.TransformationContext
) => {
return (rootNode) => {
function visit(node: ts.Node): ts.Node {
node = ts.visitEachChild(node, visit, context);
// HERE node.parent IS UNDEFINED !
return filterFn(node, context);
}
return ts.visitNode(rootNode, visit);
};
};
const transformationResult = ts.transform(
sourceFile, [transformerFactory]
);
How can I find the parent of the node?
You can parse specifying to set the parent nodes:
const sourceFile = ts.createSourceFile(
"fileName.ts",
"class Test {}",
ts.ScriptTarget.Latest,
/* setParentNodes */ true, // specify this as true
);
Or do some operation on the node to get it to set its parent nodes (ex. type check the program... IIRC during binding it ensures the parent nodes are set).
Update based on comment
If you are creating these from a program, then you can do the following:
const options: ts.CompilerOptions = { allowJs: true };
const compilerHost = ts.createCompilerHost(options, /* setParentNodes */ true);
const program = ts.createProgram([this.filePath], options, compilerHost);

Also, the action creator overrides toString() so that the action type becomes its string representation

I'm learning redux-toolkit from the official docs and came across this line- Also, the action creator overrides toString() so that the action type becomes its string representation. What does it mean?
Here's the code from the docs:
const INCREMENT = 'counter/increment'
function increment(amount) {
return {
type: INCREMENT,
payload: amount
}
}
const action = increment(3)
// { type: 'counter/increment', payload: 3 }
const increment = createAction('counter/increment')
let action = increment()
// { type: 'counter/increment' }
action = increment(3)
// returns { type: 'counter/increment', payload: 3 }
console.log(increment.toString())
// 'counter/increment'
console.log(`The action type is: ${increment}`)
// 'The action type is: counter/increment'
So, for example, when I write something like
const increment = createAction("INCREMENT")
console.log(increment.toString())
It's logging INCREMENT. So is this overriding of toString()? I'm really confused.
I'm new to redux-toolkit and any help would be appreciated. Thanks.
Normally, if you call toString() on a function, it returns the literal source text that was used to define the function:
function myFunction() {
const a = 42;
console.log(a);
}
myFunction.toString()
"function myFunction() {
const a = 42;
console.log(a);
}"
However, in this case, we want someActionCreator.toString() to return the action type that will be part of the action objects it creates:
const addTodo = createAction("todos/addTodo");
console.log(addTodo("Buy milk"));
// {type: "todos/addTodo", payload: "Buy milk"}
console.log(addTodo.toString());
// "todos/addTodo"
To make this happen, createAction overrides the actual implementation of toString for these action creators:
export function createAction(type: string): any {
function actionCreator(...args: any[]) {
return { type, payload: args[0] }
}
actionCreator.toString = () => `${type}`
actionCreator.type = type
return actionCreator;
}
This is especially useful because ES6 object literal computed properties automatically try to stringify whatever values you've passed in. So, you can now use an action creator function as the key in an object, and it'll get converted to the type string:
const reducersObject = {
[addTodo]: (state, action) => state.push(action.payload)
}
console.log(reducersObject);
// { "todos/addTodo": Function}

"Force unwrapping" in Flow?

I have this helper function in my reducer, which has the given state:
type CustomerCollection = { [number]: Customer }
type CustomerState = {
+customers: ?CustomerCollection,
+newItem: ?(Customer | Review),
+searchResults: ?(Customer[]),
+error: ?string,
+isLoading: boolean
};
function customerWithReview(review: Review): Customer {
const id: number = review.customerId;
const oldCustomer: Customer = state.customers[id];
const newReviews: Review[] = [review, ...oldCustomer.reviews];
return Object.assign(oldCustomer, { reviews: newReviews });
}
I get a Flow error on the id of const oldCustomer: Customer = state.customers[id]; saying Cannot get state.customers[id] because an index signature declaring the expected key/value type is missing in null or undefined.
This is happening because of the nullable/optional ?CustomerCollection type of state.customers.
I can silence the error by making sure customers isn't null:
if (state.customers) {
const oldCustomer: Customer = state.customers[id];
const newReviews: Review[] = [review, ...oldCustomer.reviews];
return Object.assign(oldCustomer, { reviews: newReviews });
}
But then the problem just goes up the chain because I don't have anything to return from the function.
I can certainly expand it to:
function customerWithReview(review: Review): Customer {
if (!state.customers) {
return new Customer();
} else {
const id: number = review.customerId;
const oldCustomer: Customer = state.customers[id];
const newReviews: Review[] = [review, ...oldCustomer.reviews];
return Object.assign(oldCustomer, { reviews: newReviews });
}
}
But in actual practice, the action that gets us to this branch of the reducer will never be called if state.customers is null, and we'd never return new Customer() and would have no use for it if we did. state.customers is nullable in order to tell the difference between "we haven't fetched the customers yet (state.customers == null)" and "we've fetched the customers but there are none (state.customers == {}).
It would be a lot easier if I could just assert that state.customers would always exist in these cases, which in Swift I would do with force-unwrapping:
const oldCustomer: Customer = state.customers![id];
Can I do anything like this with Flow?
Or, given that only my GET_CUSTOMERS_FAILURE action would ever deal with state.customers == null, is there some other way to restructure my reducer so that this is a little easier? An entirely separate fetchReducer that is has a nullable customer collection while the rest of the actions fall under a different reducer?
You can use invariant function (Check that it works here):
type Customer = { id: number, reviews: Array<Review> };
type Review = { customerId: number };
type CustomerCollection = { [number]: Customer }
type CustomerState = {
+customers: ?CustomerCollection,
+newItem: ?(Customer | Review),
+searchResults: ?(Customer[]),
+error: ?string,
+isLoading: boolean
};
declare var state: CustomerState;
declare function invariant(): void;
function customerWithReview(review: Review): Customer {
const id: number = review.customerId;
invariant(state.customers, 'No customers and I don\'t know why');
const oldCustomer: Customer = state.customers[id];
const newReviews: Review[] = [review, ...oldCustomer.reviews];
return Object.assign(oldCustomer, { reviews: newReviews });
}
You can implement it somewhere in your project and import when necessary.
You can implement it like this:
export function invariant<T>(value: ?T, falsyErrorMessage: string, errorParams?: Object): void {
if (!value) {
log.error(falsyErrorMessage, errorParams || {});
throw new Error(INVARIANT_ERROR_MESSAGE);
}
}
Unfortunately, the name of the function is hard-coded in flow.
Alternative variant is just to add an if and to throw an error in your customerWithReview function directly.

How can I dynamic set a value into a property?

How to set a property to value that should be resolve.. like this one..
const getDataFromServer = (id) => ({id: * 2})
R.set(payloadProp, getDataFromServer)({id: 4}); // WRONG, is setting to a function instend to resolve a function with `{id: 4}`
const fetch = (id) => {
return { num: id * 2 }
};
const idProp = R.lensProp('id');
const getDataFromServer = R.pipe(R.view(idProp), fetch);
const payloadProp = R.lensProp('payload');
const setPayloadFromFetch = R.set(payloadProp, getDataFromServer); // NOT WORK, return payload as function
const obj = { id: 1, payload: { message: 'request' } }
const ret = setPayloadFromFetch(obj);
console.log(ret);
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.23.0/ramda.min.js"></script>
The problem is that R.set takes a value, not a function, for its second parameter. And you can't switch to R.over, which does take function but calls it with the current value at that lens, not the full data object supplied to the outer function.
The simplest way to solve this is simply to pass the object in both places:
const setPayloadFromFetch = obj => R.set(payloadProp, getDataFromServer(obj), obj);
But if you're intent on making this point-free, lift is your friend:
const setPayloadFromFetch = R.lift(R.set(payloadProp))(getDataFromServer, identity);
although you could also use R.ap, which would be very nice except that R.set takes its parameters in the wrong order for it, and so you have to use R.flip
const setPayloadFromFetch = R.ap(R.flip(R.set(payloadProp)), getDataFromServer);
You can see all these in the Ramda REPL.

Maybe-type of generic type parameter is incompatible with empty

For every instantiation of RemoteEntity, I get an error on the type parameter that This type is incompatible with empty, referencing the null value for value in newRemoteEntity:
export type RemoteEntity<T: Identifiable> = {
value: ?T;
error: ?Error;
// ...
}
export function newRemoteEntity(): RemoteEntity<*> {
return {
value: null, // error
error: null, // OK
// ...
}
}
If I instead declare value: ?Object, these errors go away (but then I get other errors related to the loss of my type bound). Am I missing something or is this Flowtype bug/quirk?
I found a workaround by making the fields optional (instead of required but with a maybe-type). However, it makes other code a little more complicated (since I have to check for nulls instead of just propagating them in object literals), so I would prefer having maybe-types work.
export type RemoteEntity<T: Identifiable> = {
value?: T;
error?: Error;
pendingAction: ?string;
// ...
}
export function newRemoteEntity(): RemoteEntity<*> {
return {
pendingAction: null,
// ...
}
}
export function requested<T: Identifiable>(
state: RemoteEntity<T>, action: string, id?: string): RemoteEntity<T> {
// Maybe-type version was more concise:
// return {
// state: id && state.value && state.value.id === id ? state.value : null,
// error: null,
// pendingAction: action,
// // ...
// }
const result: RemoteEntity<T> = {
pendingAction: action,
// ...
}
if (id && state.value && state.value.id === id) {
result.value = state.value
}
return result
}

Resources