Skip to content

Commit

Permalink
Merge pull request #604 from NullVoxPopuli/better-thunk-types
Browse files Browse the repository at this point in the history
Fix type inference for the class-based resource thunk types
  • Loading branch information
NullVoxPopuli authored Sep 8, 2022
2 parents 5b73196 + e3900f7 commit e324f50
Show file tree
Hide file tree
Showing 20 changed files with 765 additions and 222 deletions.
1 change: 1 addition & 0 deletions ember-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"ember-async-data": "^0.6.0",
"ember-template-lint": "3.16.0",
"eslint": "^7.32.0",
"expect-type": "^0.13.0",
"npm-run-all": "4.1.5",
"rollup": "2.79.0",
"rollup-plugin-terser": "^7.0.2",
Expand Down
79 changes: 79 additions & 0 deletions ember-resources/src/core/-type-tests/args-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expectTypeOf } from 'expect-type';

import { Resource } from '../class-based';

import type { ArgsFrom } from '../class-based/resource';
import type { EmptyObject, Named, Positional } from '[core-types]';

// -----------------------------------------------------------
// -----------------------------------------------------------
// -----------------------------------------------------------

/**
* -----------------------------------------------------------
* Named
* -----------------------------------------------------------
*/
expectTypeOf<Named<unknown>>().toEqualTypeOf<EmptyObject>();
expectTypeOf<Named<{ named: { foo: number } }>>().toEqualTypeOf<{ foo: number }>();
expectTypeOf<Named<{ Named: { foo: number } }>>().toEqualTypeOf<{ foo: number }>();
expectTypeOf<Named<{ positional: [] }>>().toEqualTypeOf<EmptyObject>();
expectTypeOf<Named<{ Positional: [] }>>().toEqualTypeOf<EmptyObject>();
expectTypeOf<Named<{ Named: { foo: number }; Positional: [] }>>().toEqualTypeOf<{ foo: number }>();
// @ts-expect-error
expectTypeOf<Named<{ named: { foo: number }; Positional: [] }>>().toEqualTypeOf<{ foo: number }>();

/**
* -----------------------------------------------------------
* Positional
* -----------------------------------------------------------
*/
expectTypeOf<Positional<unknown>>().toEqualTypeOf<[]>();
expectTypeOf<Positional<{ positional: [number] }>>().toEqualTypeOf<[number]>();
expectTypeOf<Positional<{ Positional: [number] }>>().toEqualTypeOf<[number]>();
expectTypeOf<Positional<{ named: { foo: number } }>>().toEqualTypeOf<[]>();
expectTypeOf<Positional<{ Named: { foo: number } }>>().toEqualTypeOf<[]>();
expectTypeOf<Positional<{ Named: { foo: number }; Positional: [number] }>>().toEqualTypeOf<
[number]
>();
// @ts-expect-error
expectTypeOf<Positional<{ Named: { foo: number }; positional: [number] }>>().toEqualTypeOf<
[number]
>();

/**
* -----------------------------------------------------------
* ArgsFrom
* -----------------------------------------------------------
*/
class Foo {
foo = 'foo';
}
class Bar extends Resource {
bar = 'bar';
}
class Baz extends Resource<{ Named: { baz: string } }> {
baz = 'baz';
}
class Bax extends Resource<{ Positional: [string] }> {
bax = 'bax';
}
// {} does not extend Resource
// @ts-expect-error
expectTypeOf<ArgsFrom<{}>>().toEqualTypeOf<never>();
// unknown does not extend Resource
// @ts-expect-error
expectTypeOf<ArgsFrom<unknown>>().toEqualTypeOf<never>();
// number does not extend Resource
// @ts-expect-error
expectTypeOf<ArgsFrom<2>>().toEqualTypeOf<never>();
// string does not extend Resource
// @ts-expect-error
expectTypeOf<ArgsFrom<'string'>>().toEqualTypeOf<never>();
// Foo does not extend Resource
// @ts-expect-error
expectTypeOf<ArgsFrom<Foo>>().toEqualTypeOf<never>();

expectTypeOf<ArgsFrom<Bar>>().toEqualTypeOf<unknown>();
expectTypeOf<ArgsFrom<Baz>>().toEqualTypeOf<{ Named: { baz: string } }>();
expectTypeOf<ArgsFrom<Bax>>().toEqualTypeOf<{ Positional: [string] }>();
73 changes: 73 additions & 0 deletions ember-resources/src/core/-type-tests/thunk-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* These type tests are sorted alphabetically by the name of the type utility
*/
import { expectTypeOf } from 'expect-type';

import type { AsThunk, EmptyObject, LoosenThunkReturn, NoArgs, ThunkReturnFor } from '[core-types]';

/**
* -----------------------------------------------------------
* AsThunk - uses ThunkReturnFor + LoosenThunkReturn
* -----------------------------------------------------------
*/
expectTypeOf<AsThunk<{}>>().toEqualTypeOf<() => NoArgs | [] | EmptyObject | undefined | void>();
expectTypeOf<AsThunk<unknown>>().toEqualTypeOf<
() => NoArgs | [] | EmptyObject | undefined | void
>();
expectTypeOf<AsThunk<[]>>().toEqualTypeOf<() => NoArgs | [] | EmptyObject | undefined | void>();
expectTypeOf<AsThunk<{ foo: number }>>().toEqualTypeOf<
() => NoArgs | [] | EmptyObject | undefined | void
>();

expectTypeOf<AsThunk<{ named: { foo: number } }>>().toEqualTypeOf<
() => { named: { foo: number } } | { foo: number }
>();
expectTypeOf<AsThunk<{ named: { foo: number }; positional: [string] }>>().toEqualTypeOf<
() => { named: { foo: number }; positional: [string] }
>();
expectTypeOf<AsThunk<{ positional: [number] }>>().toEqualTypeOf<
() => { positional: [number] } | [number]
>();

/**
* -----------------------------------------------------------
* LoosenThunkReturn
* -----------------------------------------------------------
*/
expectTypeOf<LoosenThunkReturn<{ named: { foo: 1 }; positional: [] }>>().toEqualTypeOf<
{ foo: 1 } | { named: { foo: 1 } }
>();
expectTypeOf<LoosenThunkReturn<{ named: EmptyObject; positional: [string] }>>().toEqualTypeOf<
[string] | { positional: [string] }
>();
expectTypeOf<LoosenThunkReturn<{ named: { foo: 1 }; positional: [string] }>>().toEqualTypeOf<{
named: { foo: 1 };
positional: [string];
}>();

/**
* -----------------------------------------------------------
* ThunkReturnFor
* -----------------------------------------------------------
*/
expectTypeOf<ThunkReturnFor<{}>>().toEqualTypeOf<NoArgs>();
expectTypeOf<ThunkReturnFor<unknown>>().toEqualTypeOf<NoArgs>();
expectTypeOf<ThunkReturnFor<object>>().toEqualTypeOf<NoArgs>();
// How to guard against this situation?
// expectTypeOf<ThunkReturnFor<Record<string, unknown>>>().toEqualTypeOf<NoArgs>();
expectTypeOf<ThunkReturnFor<{ positional: [string] }>>().toEqualTypeOf<{
positional: [string];
named: EmptyObject;
}>();
expectTypeOf<ThunkReturnFor<{ Positional: [string] }>>().toEqualTypeOf<{
positional: [string];
named: EmptyObject;
}>();
expectTypeOf<ThunkReturnFor<{ named: { baz: string } }>>().toEqualTypeOf<{
positional: [];
named: { baz: string };
}>();
expectTypeOf<ThunkReturnFor<{ Named: { baz: string } }>>().toEqualTypeOf<{
positional: [];
named: { baz: string };
}>();
13 changes: 13 additions & 0 deletions ember-resources/src/core/-type-tests/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expectTypeOf } from 'expect-type';

import type { Class, Constructor } from '[core-types]';

class A {
a = 1;
}

/**
* Class + Constructor
*/
expectTypeOf<InstanceType<Class<A>>>().toMatchTypeOf<A>();
expectTypeOf<InstanceType<Constructor<A>>>().toMatchTypeOf<A>();
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
*
* NOTE: these examples are explicitly for testing types and may not be
* suitable for actual runtime usage
*/
import { expectTypeOf } from 'expect-type';

// import { AsThunk, Class, ContextOf, ExpandThunkReturn, Named, Positional } from '[core-types]';
import { Resource } from '../resource';

/**
* with no arguments specified
*/
class A extends Resource {
a = 1;
}

// TODO: rename to create when there are no args?
// are there use cases for class-based Resources without args?
// no args seems like it'd be easier as a function-resource.
// Valid, no args present
// A.from();
A.from(() => ({}));
A.from(() => []);

// Invalid, A does not expect args
// @ts-expect-error
A.from(() => ({ positional: [1] }));
//
// Invalid, A does not expect args
// @ts-expect-error
A.from(() => ({ named: { foo: 2 } }));

// valid, empty args are ok
A.from(() => ({ positional: [], named: {} }));

export class UsageA {
a = A.from(this, () => ({}));
a1 = A.from(this, () => []);

// Invalid, A does not expect args
// @ts-expect-error
a2 = A.from(this, () => ({ positional: [1] }));

// Invalid, A does not expect args
// @ts-expect-error
a3 = A.from(this, () => ({ named: { foo: 2 } }));

// valid, empty args are ok
a4 = A.from(this, () => ({ positional: [], named: {} }));
}

/**
* with all arguments specified
*/
type BArgs = {
positional: [num: number, greeting: string];
named: {
num: number;
str: string;
};
};

export class B extends Resource<BArgs> {
b = 'b';
}

// Valid, all arguments provided
B.from(() => {
return {
positional: [1, 'hi'],
named: { num: 2, str: 'there' },
};
});

export class UsageB {
// everything missing
// @ts-expect-error
b = B.from(this, () => ({}));

// named is missing
// @ts-expect-error
b1 = B.from(this, () => ({ positional: [1, 'hi'] }));

// positional is missing
// @ts-expect-error
b2 = B.from(this, () => ({ named: { num: 2, str: 'there' } }));

// positional is incorrect
// @ts-expect-error
b3 = B.from(this, () => ({ positional: ['hi'] }));

// named is incorrect
// @ts-expect-error
b4 = B.from(this, () => ({ named: { str: 'there' } }));

// valid -- all args present
b5 = B.from(this, () => ({ positional: [1, 'hi'], named: { num: 2, str: 'there' } }));
}

/**
* with all arguments, but capitalized (Signature style)
*/

type CArgs = {
Positional: [number, string];
Named: {
num: number;
str: string;
};
};

export class C extends Resource<CArgs> {
c = 'c';
}

/**
* The return value of the thunk has the correct type
*/
export class UsageC {
// decorator not needed for the type test (I don't want to import it)
/* @use */ cUse = C.from(() => ({ positional: [1, 'two'], named: { num: 3, str: 'four' } }));
cThis = C.from(this, () => ({ positional: [1, 'two'], named: { num: 3, str: 'four' } }));
}

expectTypeOf(new UsageC().cUse).toEqualTypeOf<C>();
expectTypeOf(new UsageC().cThis).toEqualTypeOf<C>();
Loading

0 comments on commit e324f50

Please sign in to comment.