Skip to content

Commit

Permalink
feat(types, internal): add type tests, try to re-widen typescript ran…
Browse files Browse the repository at this point in the history
…ge to bring back support for TS 4.5+
  • Loading branch information
NullVoxPopuli committed Jun 22, 2022
1 parent a347f1d commit bc33140
Show file tree
Hide file tree
Showing 22 changed files with 326 additions and 69 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ jobs:
fail-fast: true
matrix:
typescript-scenario:
- [email protected]
- [email protected]
- [email protected]
steps:
- uses: actions/checkout@v3
Expand All @@ -239,9 +241,9 @@ jobs:
run: pnpm add --save-dev ${{ matrix.typescript-scenario }}
working-directory: ./testing/ember-app
- name: Type checking
run: |-
pnpm --filter ember-app exec glint --version;
pnpm --filter ember-app exec glint
run: >-
pnpm --filter ember-app exec tsc -v; pnpm --filter ember-app exec
glint --version; pnpm --filter ember-app exec glint
release:
name: Release
timeout-minutes: 5
Expand Down
2 changes: 2 additions & 0 deletions ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ support:
ember-try: true
glint: true
typescript:
- [email protected]
- [email protected]
- [email protected]
# MSW is not compatible with typescript@next
# - typescript@next
Expand Down
13 changes: 7 additions & 6 deletions ember-resources/src/core/class-based/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { invokeHelper } from '@ember/helper';

import { DEFAULT_THUNK, normalizeThunk } from '../utils';

import type { ArgsWrapper, Cache, Thunk } from '../types';
import type { Cache, Thunk } from '../types';
import type { Named, Positional } from './types';
import type { HelperLike } from '@glint/template';
// this lint thinks this type import is used by decorator metadata...
// babel doesn't use decorator metadata
Expand All @@ -19,11 +20,11 @@ import type { Invoke } from '@glint/template/-private/integration';
*
* This is a Glint helper to help HelperLike determine what the ReturnType is.
*/
type ResourceHelperLike<T extends ArgsWrapper, R> = InstanceType<
type ResourceHelperLike<T, R> = InstanceType<
HelperLike<{
Args: {
Named: T['named'];
Positional: T['positional'];
Named: Named<T>;
Positional: Positional<T>;
};
Return: R;
}>
Expand Down Expand Up @@ -105,7 +106,7 @@ type ResourceHelperLike<T extends ArgsWrapper, R> = InstanceType<
* This way, consumers only need one import.
*
*/
export class Resource<T extends ArgsWrapper = ArgsWrapper> {
export class Resource<T = unknown> {
/**
* @private (secret)
*
Expand Down Expand Up @@ -181,7 +182,7 @@ export class Resource<T extends ArgsWrapper = ArgsWrapper> {
* this lifecycle hook is called whenever arguments to the resource change.
* This can be useful for calling functions, comparing previous values, etc.
*/
modify?(positional: T['positional'], named: T['named']): void;
modify?(positional?: Positional<T>, named?: Named<T>): void;
}

function resourceOf<Instance extends new (...args: any) => any, Args extends unknown[] = unknown[]>(
Expand Down
54 changes: 54 additions & 0 deletions ember-resources/src/core/class-based/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* All of this is baseds off the types for @glimmer/component
* (post starting official typescript support in ember)
*/

// Type-only "symbol" to use with `EmptyObject` below, so that it is *not*
// equivalent to an empty interface.
declare const Empty: unique symbol;

/**
* This provides us a way to have a "fallback" which represents an empty object,
* without the downsides of how TS treats `{}`. Specifically: this will
* correctly leverage "excess property checking" so that, given a component
* which has no named args, if someone invokes it with any named args, they will
* get a type error.
*
* @internal This is exported so declaration emit works (if it were not emitted,
* declarations which fall back to it would not work). It is *not* intended for
* public usage, and the specific mechanics it uses may change at any time.
* The location of this export *is* part of the public API, because moving it
* will break existing declarations, but is not legal for end users to import
* themselves, so ***DO NOT RELY ON IT***.
*/
export type EmptyObject = { [Empty]?: true };

type GetOrElse<Obj, K, Fallback> = K extends keyof Obj ? Obj[K] : Fallback;

type ArgsFor<S> =
// Signature['Args']
S extends { Named?: object; Positional?: unknown[] }
? {
Named: GetOrElse<S, 'Named', EmptyObject>;
Positional: GetOrElse<S, 'Positional', []>;
}
: S extends { named?: object; positional?: unknown[] }
? {
Named: GetOrElse<S, 'named', EmptyObject>;
Positional: GetOrElse<S, 'positional', []>;
}
: { Named: EmptyObject; Positional: [] };

/**
* Converts a variety of types to the expanded arguments type
* that aligns with the 'Args' portion of the 'Signature' types
* from ember's helpers, modifiers, components, etc
*/
export type ExpandArgs<T> = T extends any[]
? ArgsFor<{ Positional: T }>
: T extends any
? ArgsFor<T>
: never;

export type Positional<T> = ExpandArgs<T>['Positional'];
export type Named<T> = ExpandArgs<T>['Named'];
53 changes: 2 additions & 51 deletions ember-resources/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,8 @@
export type Fn = (...args: any[]) => any;

/**
* This shorthand is 3 character shorter than using `positional:` in ArgsWrapper
*
* @example
*
* ```ts
* import { Resource } from 'ember-resources';
*
* import type { Positional } from 'ember-resources';
*
* class MyResource extends Resource<Positional<[number]>> {
*
* }
* ```
*
*
*/
export interface Positional<T extends Array<unknown>> {
positional: T;
}

/**
* This shorthand is 3 character shorter than using `named:` in ArgsWrapper
*
* @example
*
* ```ts
* import { Resource } from 'ember-resources';
*
* import type { Named } from 'ember-resources';
*
* class MyResource extends Resource<Named<{ bananas: number }>> {
*
* }
* ```
*
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export interface Named<T extends {} = Record<string, unknown>> {
named: T;
}

/**
* This is a utility interface that represents all of the args used throughout
* Ember.
* This is a utility interface that represents the resulting args structure after
* the thunk is normalized.
*
* @example
* ```ts
Expand All @@ -66,13 +24,6 @@ export interface ArgsWrapper {
named?: Record<string, any>;
}

export interface SignatureFor<T extends ArgsWrapper> {
Args: {
Positional: T['positional'];
Named: T['named'];
};
}

// typed-ember should provide this from
// @glimmer/tracking/primitives/cache
// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down
3 changes: 2 additions & 1 deletion ember-resources/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { resource, resourceFactory } from './core/function-based';
export { use } from './core/use';

// Public Type Utilities
export type { ArgsWrapper, Named, Positional, Thunk } from './core/types';
export type { ExpandArgs } from './core/class-based/types';
export type { ArgsWrapper, Thunk } from './core/types';
4 changes: 2 additions & 2 deletions ember-resources/src/util/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ export function map<Element = unknown, MapTo = unknown>(
) {
let { data, map } = options;

let resource = TrackedArrayMap<Element, MapTo>.from(destroyable, () => {
let resource = TrackedArrayMap.from(destroyable, () => {
let reified = data();

return { positional: [reified], named: { map } };
});
}) as TrackedArrayMap<Element, MapTo>;

/**
* This is what allows square-bracket index-access to work.
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions testing/ember-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"prettier": "^2.7.1",
"qunit": "^2.16.0",
"qunit-dom": "^2.0.0",
"ts-expect": "^1.3.0",
"typescript": "^4.7.2",
"webpack": "^5.72.1"
},
Expand Down
8 changes: 3 additions & 5 deletions testing/ember-app/tests/core/js-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import { setupTest } from 'ember-qunit';

import { Resource } from 'ember-resources';

import type { Positional } from 'ember-resources';

// not testing in template, because that's the easy part
module('Core | Resource | js', function (hooks) {
setupTest(hooks);

test('it works', async function (assert) {
class Doubler extends Resource {
class Doubler extends Resource<{ positional: [number] }> {
@tracked num = 0;

modify([passedNumber]: [number]) {
Expand Down Expand Up @@ -74,7 +72,7 @@ module('Core | Resource | js', function (hooks) {
});

test('can take a typed array https://github.com/NullVoxPopuli/ember-resources/issues/48', async function (assert) {
class DoubleEverything extends Resource<Positional<number[]>> {
class DoubleEverything extends Resource<{ positional: number[] }> {
@tracked result: number[] = [];

modify(positional: number[]) {
Expand Down Expand Up @@ -107,7 +105,7 @@ module('Core | Resource | js', function (hooks) {
registerDestructor(this, () => assert.step('teardown'));
}

modify(positional: number[]) {
modify(positional: [number]) {
assert.step('modify');
this.num = positional[0] * 2;
}
Expand Down
2 changes: 1 addition & 1 deletion testing/ember-app/tests/core/rendering-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module('Core | Resource | rendering', function (hooks) {
class Doubler extends Resource<{ positional: [number] }> {
@tracked num = 0;

modify(positional: number[]) {
modify(positional: [number]) {
this.num = positional[0] * 2;
}
}
Expand Down
41 changes: 41 additions & 0 deletions testing/ember-app/tests/type-tests/class-based.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Resource } from 'ember-resources';
import { expectType } from 'ts-expect';

class A extends Resource {
a = 1;
}

expectType<number>(A.from({}).a);
expectType<A>(A.from({}));

type BArgs = {
positional: [number, string];
named: {
num: number;
str: string;
};
};

export class B extends Resource<BArgs> {
modify(positional: BArgs['positional'], named: BArgs['named']) {
expectType<[number, string]>(positional);
expectType<number>(named.num);
expectType<string>(named.str);
}
}

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

export class C extends Resource<CArgs> {
modify(positional: CArgs['Positional'], named: CArgs['Named']) {
expectType<[number, string]>(positional);
expectType<number>(named.num);
expectType<string>(named.str);
}
}
15 changes: 15 additions & 0 deletions testing/ember-app/tests/type-tests/function-based.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { resource } from 'ember-resources';
import { expectType } from 'ts-expect';

/*
* These are all lies, but... useful lies.
*
* in JS, we require the use of a @use decorator
*
* in templates, there is a whole rendering system that figures out the value.
*
* In both situations, the effective value *is* the string
*
*/
expectType<string>(resource(() => 'hi'));
expectType<string>(resource(() => () => 'hi'));
Loading

0 comments on commit bc33140

Please sign in to comment.