-
-
Notifications
You must be signed in to change notification settings - Fork 0
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
Typescript types #1
Comments
cc @dylan1p |
@Qix- I think it might be possible, but It's not really clear to be what the rules should be.
So what matters to the compatibility of these generators ? The current issue is that What about the other parameters ? Is the value returned by |
Sorry for the delay on this; this is turning out to be a really tricky comment to word correctly since the types are quite hairy. Hopefully you're still interested in helping @dragomirtitian 😅 Before continuing, let me define a term so that we're on the same page: while the string keys and their function values are just that - functions - I'll refer to the set of functions that share the same key among any of the objects in which that key is present as a "method". const cat = {
meow: async function *() {},
walk: async function *() {}
};
const human = {
speak: async function *() {},
walk: async function *() {}
};
const result = scaly([human, cat]); In the above, there are two layers - Hence, Further, I'll refer to the async generator declarations as input methods and the single "aggregate" resulting method as the output method.
Let me annotate an example with inline comments explaining why the types are what they are. import scaly from 'scaly';
/*
Three output methods:
- async getName(string) : string
- async getLocation(string): [string, string]
- async setName(string) : string|null
*/
const gpsLayer = {
async *getLocation(id: string) {
try {
const loc: [string, string] = await someGPSClient.find(id);
return loc;
} catch (err) {
if (err.code == 'UNKNOWN_ID') {
// "throw" a recoverable error by `yield`-ing a non-undefined value.
// This form of `yield expr` never resumes (returns).
// Note that we yield an `Error` object here, not a `string`.
yield err; // yield expr (where typeof(expr) is anything except undefined)
} else {
// otherwise, throw a real exception.
throw err; // not covered by typescript I don't think.
}
}
}
};
const mongoLayer = {
async *getName(id: string) {
const name: string|undefined = await someMongoClient.find({id}, {name: 1});
if (!name) yield 'unknown user ID'; // "throw" a recoverable error again
return name;
},
async *setName(id: string, name: string) {
const oldName: string|undefined = await someMongoClient.upsertAndGetOldValue({id}, {name});
return oldName || null; // return the string if it's there, or `null` if it's falsey
}
};
// This is where the use of `undefined` becomes important.
const redisLayer = {
async *getName(id: string) {
const name: string|undefined = await someRedisClient.get(`${id}:name`);
// If it's a cache-hit, then simply return it
if (name) {
return name; // typeof(name) == string, same as mongoLayer's `getName()` method type
} else {
// Otherwise, produce the value from the next layer.
// The result of a naked `yield` is never `undefined` because
// Scaly will error if no result can be produced from any of the
// subsequent layers.
const newName: string = yield; // same as the method return type, never `undefined`.
await someRedisClient.set(`${id}:name`, newName);
// In this case, the return value can be `undefined` since Scaly
// already has the result value. There is no need to `return newName`
// again a second time.
return undefined;
}
},
async *getLocation(id: string) {
const loc : [string, string]|undefined = await someRedisClient.get(`${id}:location`);
if (loc) {
return loc; // typeof(loc) == [string, string], same as gpsLayer's `getLocation()`
} else {
// Same thing here.
const newLoc: [string, string] = yield;
await someRedisClient.set(`${id}:location`, newLoc);
return undefined;
}
},
async *setName(id: string, name: string) {
// This is the strange one; in our implementation here,
// we just want to observe the calls to `setName()` without
// producing a result and letting the later layers
// have a chance at using the parameter, too.
//
// Even though we don't return a result in the implementation,
// the return type should still be string|null|undefined since that's
// what the other layers' inner method return types are (specifically,
// mongoLayer.setName(string, string): string|null).
//
// Therefore, there are two ways we can write this
// implementation:
{
// The first way is to handle it ourselves first,
// then let the function return `undefined` to
// indicate we don't care about the result
// nor do we have a result or an error to produce,
// and that the next layers, if any, should be invoked:
await someRedisClient.set(`${id}:name`, name);
return undefined;
}
{
// Alternatively, we can let the deeper layers handle
// the request first, and if there is no error result
// produced, we can then handle it *after* the other
// layers have handled it:
yield; // discard the result (which would otherwise be type `string|null`)
await someRedisClient.set(`${id}:name`, name);
return undefined;
}
}
};
const DB = scaly([
redisLayer,
mongoLayer,
gpsLayer
]);
/*
typeof(DB) == {
getName: async function (string): [true, string] | [false, string],
setName: async function (string, string): [true, string|null] | [false, unknown],
getLocation: async function (string): [true, [string, string]] | [false, Error]
}
*/ The resulting object's function values' return types are a tuple of Hopefully this makes sense. I perfectly understand if this is something you're not interested in doing 😅 Scaly is definitely the most complicated libraries I've personally seen when it comes to type munging so I can understand not wanting to do it. I'm mostly interested in base-level correctness - for example, if you can only express the resulting methods' return types as As always, any information is helpful even if it's short of the full typings, haha. Though I think it's a fun puzzle for those interested in cryptic Typescript type definitions! Thanks again for the response and the help on SO! |
cc @adamhaile - S.js/Surplus must have some crazy hairy typing, maybe you can shed some light on how to approach this. :D |
Got a new job where I'll be writing Typescript and needed a good project to refresh my understanding. After a few hours, I can say I've slapped these together.. decently enough. Released as |
cc @dragomirtitian - I was the one who asked this StackOverflow question - perhaps you can further help me.
Also cc @blakeembrey, since you've helped me in the past with
ZEIT'sVercel'sarg
package :)Here are the types we've come up with so far, thanks to @dragomirtitian's help:
However, it breaks down pretty quickly once you start using the library as it's intended.
Take the following example:
The above should output:
But typescript is choking on the types:
Which... you weren't kidding about the hairy error output.
The tl;dr here is that
yield
andreturn
are both entirely optional, and the types are affected based on how the function uses them:return;
(no return value) is acceptable, always. It's a special signal.return expr;
is acceptable, but theexpr
type must match between all layers. It is what becomes thePromise
tuple's second type (hence the edit to the SO question) - hence thePromise<[boolean, infer R]>
.yield
(with nothing yielded) is acceptable and yields the same type as the return type (theinfer R
bit).yield expr;
is acceptable, but never returns (not sure if this makes a difference with typescript) and can beany
(a string,Error
, whatever) - it is treated as a "gracefulthrow
" in Scaly's terms.Of course, the methods can also
throw
but Scaly treats those as irrecoverable/internal/fatal errors, not user-generated errors.The existing types get close (remove
getError()
and the call to it, and the types are happy and correct) but I'm really afraid we won't be able to achieve type correctness for this weird API without requiring we add: AsyncGenerator<...>
return type annotations to everything (which would be highly annoying).The other thing I want to avoid is e.g. one implementation decides it wants to yield an error, that all of the other implementations now have to update their type definitions. It would be great if it could error when there's a violation of type symmetry where it counts, but doesn't error when otherwise inconsequential things like that happen.
For example, the Mongo layer might all of a sudden
yield new SomeMongoErrorType('error')
forgetName()
. I don't want to have to update the Redis layer'sgetName()
signature to includeSomeRedisErrorType | SomeMongoErrorType
. Fast forward to 20 layers, and these types become unwieldly.Am I dreaming of a utopia, or is this possible with Typescript?
The text was updated successfully, but these errors were encountered: