diff --git a/types/core-object/-private/utils.d.ts b/types/core-object/-private/utils.d.ts new file mode 100644 index 0000000000..35e3880150 --- /dev/null +++ b/types/core-object/-private/utils.d.ts @@ -0,0 +1,40 @@ +export type Constructor = new (...args: any[]) => Instance; +export type Mix = U & Pick>; +export type Values = T[keyof T]; + +/** Just the strings corresponding to method names on the given type */ +export type MethodNames = Values<{ [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never }>; + +/** + * Given a `this` type, arg types tuple and return type, creates a function-ish + * type that can only be only be invoked via `.call` or `.apply`. Because of the + * way CoreObject's `_super` works, it's an error to write e.g. `this._super.init()`; + */ +export interface CallOrApply { + apply: (thisArg: This, args: Args extends undefined ? any[] : Args | IArguments) => Return; + + // TODO support this properly with `...args: Args` once we can restrict to 3.0+ on DT + call: + Args extends undefined ? (thisArg: This) => Return : + Args extends [infer A] ? (thisArg: This, a: A) => Return : + Args extends [infer A, infer B] ? (thisArg: This, a: A, b: B) => Return : + Args extends [infer A, infer B, infer C] ? (thisArg: This, a: A, b: B, c: C) => Return : + Args extends [infer A, infer B, infer C, infer D] ? (thisArg: This, a: A, b: B, c: C, d: D) => Return : + (thisArg: This, ...args: any[]) => Return; +} + +/** + * The type of `this._super`, which has keys for all methods appearing in the given + * type, but forces the caller to use `.call` or `.apply` to invoke them. + */ +export type Super = { + // TODO just do `infer Args` once we can restrict to 3.0+ on DT + [K in MethodNames]: + T[K] extends () => infer Return ? CallOrApply : + T[K] extends (a: infer A) => infer Return ? CallOrApply : + T[K] extends (a: infer A, b: infer B) => infer Return ? CallOrApply : + T[K] extends (a: infer A, b: infer B, c: infer C) => infer Return ? CallOrApply : + T[K] extends (a: infer A, b: infer B, c: infer C, d: infer D) => infer Return ? CallOrApply : + T[K] extends (...args: any[]) => infer Return ? CallOrApply : + never; +}; diff --git a/types/core-object/core-object-tests.ts b/types/core-object/core-object-tests.ts new file mode 100644 index 0000000000..b434ee97fc --- /dev/null +++ b/types/core-object/core-object-tests.ts @@ -0,0 +1,163 @@ +import { Mix, MethodNames } from 'core-object/-private/utils'; +import CoreObject, { ExtendOptions, ExtendThisType } from 'core-object'; + +//////////// Mix //////////// + +declare const mix1: Mix<{ a: number }, { b: string }>; +mix1.a; // $ExpectType number +mix1.b; // $ExpectType string + +declare const mix2: Mix<{ a: number }, { a: string }>; +mix2.a; // $ExpectType string +mix2.b; // $ExpectError + +//////////// MethodNames //////////// + +declare const names1: MethodNames<{ a: string }>; +names1; // $ExpectType never + +declare const names2: MethodNames<{ a: () => number; b(arg: number): void }>; +names2; // $ExpectType "a" | "b" + +declare const names3: MethodNames<{ a: () => 'hi'; b: null }>; +names3; // $ExpectType "a" + +//////////// ExtendOptions //////////// + +const extendOptions1: ExtendOptions<{}> = { a: 1, b: 'hi' }; +const extendOptions2: ExtendOptions<{ a: number }> = { b: 'hi' }; +const extendOptions3: ExtendOptions<{ a: number }> = { a: 5 }; +const extendOptions4: ExtendOptions<{ a: number }> = { a: 'hi' }; // $ExpectError + +//////////// ExtendThisType //////////// + +declare function extendThisType1(options: T & ExtendThisType<{ prop: string; method: () => number }, T>): void; +extendThisType1({ + otherMethod() { + this.prop; // $ExpectType string + this.random; // $ExpectError + + this._super.method.call(this); // $ExpectType number + this._super.random; // $ExpectError + } +}); + +//////////// CoreObject //////////// + +const A = CoreObject.extend({ + foo: 'hello', + + method(): string { + return this.foo; + } +}); + +const a = new A(); +a.foo; // $ExpectType string +a.bar; // $ExpectError +a.method(); // $ExpectType string + +const B = A.extend({ + bar: 123, + + other(): string { + return this._super.method.call(this) + this.foo; + } +}); + +const b = new B(); +b.foo; // $ExpectType string +b.bar; // $ExpectType number +b.method(); // $ExpectType string +b.other(); // $ExpectType string + +class ClassWithMethods extends CoreObject.extend({ + extendMethod(arg: number): string { + return 'ok'; + } +}) { + esMethod(arg: string): number { + return 123; + } +} + +const ExtendSubclass = ClassWithMethods.extend({ + anotherMethod() { + this._super.extendMethod(1); // $ExpectError + this._super.esMethod('hi'); // $ExpectError + + this._super.extendMethod.call(this, 1); // $ExpectType string + this._super.extendMethod.apply(this, [1]); // $ExpectType string + + this._super.esMethod.call(this, 'hi'); // $ExpectType number + this._super.esMethod.apply(this, ['hi']); // $ExpectType number + } +}); + +class ESSubclass extends ClassWithMethods { + anotherMethod() { + this._super; // $ExpectError + + super.extendMethod(1); // $ExpectType string + super.esMethod('hi'); // $ExpectType number + } +} + +ClassWithMethods.extend({ extendMethod: null }); // $ExpectError +ClassWithMethods.extend({ extendMethod() {} }); // $ExpectError +ClassWithMethods.extend({ esMethod: null }); // $ExpectError +ClassWithMethods.extend({ esMethod() {} }); // $ExpectError + +ClassWithMethods.extend({ + extendMethod(arg: number): string { + const result = this._super.extendMethod.call(this, arg); + result; // $ExpectType string + return result; + }, + + esMethod(arg: string): number { + const result = this._super.esMethod.call(this, arg); + result; // $ExpectType number + return result; + } +}); + +declare class ClassWithManyMethods extends CoreObject { + method0(): 0; + method1(a: 'a'): 1; + method2(a: 'a', b: 'b'): 2; + method3(a: 'a', b: 'b', c: 'c'): 3; + method4(a: 'a', b: 'b', c: 'c', d: 'd'): 4; + method5(a: 'a', b: 'b', c: 'c', d: 'd', e: 'e'): 5; +} + +ClassWithManyMethods.extend({ + child(): void { + this._super.method0.call(this); // $ExpectType 0 + this._super.method0.apply(this, []); // $ExpectType 0 + + this._super.method1.call(this, 'a'); // $ExpectType 1 + this._super.method1.apply(this, ['a']); // $ExpectType 1 + this._super.method1.call(this, 'x'); // $ExpectError + this._super.method1.apply(this, ['x']); // $ExpectError + + this._super.method2.call(this, 'a', 'b'); // $ExpectType 2 + this._super.method2.apply(this, ['a', 'b']); // $ExpectType 2 + this._super.method2.call(this, 'a', 'x'); // $ExpectError + this._super.method2.apply(this, ['a', 'x']); // $ExpectError + + this._super.method3.call(this, 'a', 'b', 'c'); // $ExpectType 3 + this._super.method3.apply(this, ['a', 'b', 'c']); // $ExpectType 3 + this._super.method3.call(this, 'a', 'b', 'x'); // $ExpectError + this._super.method3.apply(this, ['a', 'b', 'x']); // $ExpectError + + this._super.method4.call(this, 'a', 'b', 'c', 'd'); // $ExpectType 4 + this._super.method4.apply(this, ['a', 'b', 'c', 'd']); // $ExpectType 4 + this._super.method4.call(this, 'a', 'b', 'c', 'x'); // $ExpectError + this._super.method4.apply(this, ['a', 'b', 'c', 'x']); // $ExpectError + + // Arity 4 is as high as we go for arg checking + this._super.method5.call(this, 'foo', 'bar', 'baz'); // $ExpectType 5 + this._super.method5.apply(this, ['foo', 'bar', 'baz']); // $ExpectType 5 + } +}); diff --git a/types/core-object/index.d.ts b/types/core-object/index.d.ts new file mode 100644 index 0000000000..aa4566cac2 --- /dev/null +++ b/types/core-object/index.d.ts @@ -0,0 +1,23 @@ +// Type definitions for core-object 3.0 +// Project: https://github.com/ember-cli/core-object +// Definitions by: Dan Freeman +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.8 + +import { Mix, Super, Constructor } from './-private/utils'; + +/** The type of options allowed to be passed to `Base.extend()` */ +export type ExtendOptions = { [K in keyof Base]?: Base[K] } & Record; + +/** The `this` type for any methods on the options passed to `Base.extend()` */ +export type ExtendThisType = ThisType & { _super: Super }>; + +export default class CoreObject { + // TODO restrict to `Record` once we can restrict to 3.0+ on DT + init(options?: Record): void; + + static extend, Ext extends ExtendOptions>>( + this: BaseClass, + options: Ext & ExtendThisType, Ext> + ): BaseClass & Constructor, Ext>>; +} diff --git a/types/core-object/tsconfig.json b/types/core-object/tsconfig.json new file mode 100644 index 0000000000..908527fc38 --- /dev/null +++ b/types/core-object/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "-private/utils.d.ts", + "core-object-tests.ts" + ] +} diff --git a/types/core-object/tslint.json b/types/core-object/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/core-object/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" }