microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.26k stars 12.31k forks source link

Non‑`void` returning assertion functions #40562

Open ExE-Boss opened 3 years ago

ExE-Boss commented 3 years ago

Search Terms

Suggestion

A way to type a function that is both an assertion function and returning a value that is not void.

Use Cases

This is necessary to correctly type Jest’s expect(…) matchers, which have a generic return type of R.

Examples

declare const expect: <T>(actual: T): JestMatchers<T>;

type JestMatchers<T> = JestMatchersShape<
    Matchers<void, T>,
    Matchers<
        Promise<void>,
        T extends PromiseLike<infer U>
            ? U
            : Exclude<T, PromiseLike<any>>
    >
>;

type JestMatchersShape<TNonPromise extends {} = {}, TPromise extends {} = {}> = {
    resolves: AndNot<TPromise>;
    rejects: AndNot<TPromise>;
} & AndNot<TNonPromise>

type AndNot<T> = T & { not: T };

interface Matchers<R, T = {}> {
    toBe<E>(expected: E): R & (asserts T is E);
}

declare const foo: unknown;
expect(foo).toBe("foo");
foo; // $ExpectType "foo"
// Some typings for engine262's Completion Record handling:
type UnwrapNormalCompletion<T>
    = unknown extends T ? Value | undefined
    : T extends NormalCompletion<infer V>
        ? (unknown extends V ? Value | undefined : V)
    : T extends Completion ? never
    : T;

/** @see https://tc39.es/ecma262/#sec-returnifabrupt */
export declare function ReturnIfAbrupt<T>(completion: T):
    (UnwrapNormalCompletion<T>)
    & (asserts completion is UnwrapNormalCompletion<T>);

/** @see https://tc39.es/ecma262/#sec-returnifabrupt-shorthands */
export { ReturnIfAbrupt as Q };

/**
 * The type signature is the same, but `AssertNormalCompletion` causes an error to be thrown at runtime
 * if `argument` is an AbruptCompletion, whereas `ReturnIfAbrupt` gets replaced with code that causes
 * the caller to return the AbruptCompletion by engine262's build system:
 *
 * @see https://tc39.es/ecma262/#sec-returnifabrupt
 * @see https://tc39.es/ecma262/#sec-returnifabrupt-shorthands
 */
declare function AssertNormalCompletion<T>(completion: T):
    (UnwrapNormalCompletion<T>)
    & (asserts completion is UnwrapNormalCompletion<T>);
export { AssertNormalCompletion as X };

Checklist

My suggestion meets these guidelines:

jcalz commented 3 years ago

duplicate of or related to #34636

ttencate commented 3 years ago

Note that the duplicate #34636 (which got here first) has 39 :+1: at the time of writing, and this one has only 9 :+1:. If these are used for prioritization, perhaps this one should be closed and the other should be reopened?

RebeccaStevens commented 2 years ago

Here's a simpler example that doesn't require any 3rd party libraries:

type ValidRawData = { foo: string; };
declare function assertIsValidRawData(value: unknown): asserts value is ValidRawData;

// Ideal return type would be some like:
// ParsedData & asserts data is ValidRawData
function parseData(data: unknown) {
    assertIsValidRawData(
        data !== null &&
        typeof data === "object" &&
        Object.hasOwn(data, "foo") &&
        typeof data.foo === "string"
    );
    ...
}

const rawData: unknown = {};
const data = parseData(rawData);
const foo = rawData.foo; // <== Not currently valid. `parseData` called above should be able to assert that rawData is of type ValidRawData.

Playground link.

natew commented 2 years ago

One footgun this avoids is it can avoid unused variables:

const val = api.value
ensureExists(val)
// if you never use val for rest of file, but linter is happy
const val = ensureExists(api.value)
// if you never use val for rest of file, linter will complain / editor will show unused
RebeccaStevens commented 2 years ago

@natew If you never use val, why define it? just call the function.

ensureExists(api.value)
natew commented 2 years ago

You miss the whole point of the post...

This can happen if you use val for something valid, then clean up some code below it that removes all the references to it, for example. You miss seeing its not necessary anymore because it tricks the linter. Whereas the assign pattern if I remove all references below I’ll clearly see it’s dangling.

On May 20, 2022, at 11:25 AM, Rebecca Stevens @.***> wrote:

 @natew If you never use val, why define it? just call the function.

ensureExists(api.value) — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.

irwincollin commented 2 months ago

Would love for this feature to exist. Not only would it be useful for some mutating functions like Object.assign and the jest functions above, but I think it also enables some novel compile-time safe patterns for mutable structures that aren't possible to represent currently.

For example, a state machine that can assert that a transition is changing the underlying object to a new state, and return information about the transition itself.

Imagine this example:

myMachine = createMachine();

myMachine.stopExport(); // error: stopExport does not exist on type MachineReady

myMachine.startExport() // returns returns a promise to await the export AND asserts that myMachine is in a different state
myMachine.startExport() // error: startExport does not exist on MachineStarted
ITenthusiasm commented 1 month ago

I agree that this would be a helpful feature. In addition to the Object.assign() scenario that @irwincollin mentioned above, there may be situations where a developer wants to mutate an array in place in order to avoid the performance overhead that comes from methods like Array.map. (Array.map would consume additional memory unnecessarily since it always creates a new array, and this can be problematic if the array is very large.)

The problem with returning something like T & U (like Object.assign()) is that such a return type won't communicate that the original variable which was passed to the function was mutated. And the problem with asserts is that it can't be used to return the mutated object for convenience.

A syntax like

function mapArrayInPlace<T, U>(array: T[], func: (items: T) => U): mutates array to U[];

could be helpful. I would hope that this would be fairly simple since TypeScript already has a way to assert and a way to return generic types. We'd just be adding 2 new keywords, and it would look familiar to what we have in other situations:

function predicate(arg: unknown): arg is number;
function asserter(arg: unknown): asserts arg is number;
function mutator(arg: unknown): mutates arg to number;
ITenthusiasm commented 1 month ago

This seems to be related to #17181, though it comes from the opposite angle. A library that rigorously identifies its mutating functions in the way described above could satisfy some of the requirements of that issue if I'm understanding correctly. (I only skimmed the issue.)