Skip to content

Keyof includes class methods #18133

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

Closed
reaperdtme opened this issue Aug 30, 2017 · 8 comments
Closed

Keyof includes class methods #18133

reaperdtme opened this issue Aug 30, 2017 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@reaperdtme
Copy link

reaperdtme commented Aug 30, 2017

TypeScript Version: 2.4.2

Code

class Model {
    public id: string
    public createdAt: Date
    public lastModified: Date
    version: number
    private transient: Object

   save(): Promise<this> {

   }
}
enum Type {
    string,
    number,
    date
    // ...
}

type Schema<T extends Model> = { [P in keyof T]: Type }

let schema: Schema<Model> = {
   id: Type.string,
   createdAt: Type.date,
   lastModified: Type.date,
   version: Type.number
}

Expected behavior:
Expected behavior here is that keyof follows own enumerable types of class, and thus, schema is validly typed. Note, that a Partial is not an option since this example needs all properties to be defined in schema.

Actual behavior:

TypeTest.ts(20,5): error TS2322: Type '{ id: Type.string; createdAt: Type.date; lastModified: Type.date; version: Type.number; }' is not assignable to type 'Schema<Model>'.
  Property 'save' is missing in type '{ id: Type.string; createdAt: Type.date; lastModified: Type.date; version: Type.number; }'.
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Aug 30, 2017
@RyanCavanaugh
Copy link
Member

keyof doesn't distinguish methods vs properties

@gcnew
Copy link
Contributor

gcnew commented Aug 30, 2017

There is a workaround/hacky way to filter methods out using type level programming. The following works with the latest nightly build of the compiler.

type Bool = 'true' | 'false'

type Not<X extends Bool> = {
    true: 'false',
    false: 'true'
}[X]

type HaveIntersection<S1 extends string, S2 extends string> = (
    { [K in S1]: 'true' } &
    { [key: string]: 'false' }
)[S2]

type IsNeverWorker<S extends string> = (
    { [K in S]: 'false' } &
    { [key: string]: 'true' }
)[S]

// Worker needed because of https://github.com/Microsoft/TypeScript/issues/18118
type IsNever<T extends string> = Not<HaveIntersection<IsNeverWorker<T>, 'false'>>

type IsFunction<T> = IsNever<keyof T>

type NonFunctionProps<T> = {
    [K in keyof T]: {
        'false': K,
        'true': never
    }[IsFunction<T[K]>]
}[keyof T]

class Model {
    public id: string
    public createdAt: Date
    public lastModified: Date
    version: number
    private transient: Object

    save(): Promise<this> {
       return null!;
    }
}
enum Type {
    string,
    number,
    date
    // ...
}

type Schema<T extends Model> = { [P in NonFunctionProps<T>]: Type }

let schema: Schema<Model> = {
   id: Type.string,         // <-- try commenting me out for a type error
   createdAt: Type.date,
   lastModified: Type.date,
   version: Type.number
}

@olegdunkan
Copy link

@gcnew it is interesting why

var x: NonFunctionProps<Model>;

produces x of never but inside mapped type it works?

@gcnew
Copy link
Contributor

gcnew commented Aug 30, 2017

Which compiler version are you using? I have 2.6.0-dev.20170829 and it works as expected.

type X = NonFunctionProps<Model>;   // 'id' | 'createdAt' | 'lastModified' | 'version'

@olegdunkan
Copy link

@gcnew yes, you are right, I have tried it at 2.5.1 version, now I've updated master branch and it works, thanks!

@reaperdtme
Copy link
Author

@RyanCavanaugh why is that? considering that save() is not actually a property or owned by any instance of Model, and that Javascript respects that, why would it be typed that way?

Example:

class Test {
	hello() {
		console.log('hello')
	}
}
let it = new Test()
it.hello() // hello
console.log(Object.keys(t)) // []

hello is not a member of it

t.hello = function() { 
   console.log('world')
}
t.hello() // world
console.log(Object.keys(t)) // ['hello']

after assigning hello to it, hello is an actual member of it, and javascript reflects that

@DanielRosenwasser
Copy link
Member

"hello" is not returned from Object.keys, but

  1. That's fine because returning fewer keys than there are elements in a union type is permissible (i.e. returning "foo" if you promise "foo" | "bar" is just fine).
  2. Object.keys can return keys that are not in keyof Foo in the first place, so trying to make the parallel is tempting but overall misleading.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 31, 2017

class Test {
	hello() {
		console.log('hello')
	}
}
let it = new Test()
it.hello() // hello
console.log(Object.keys(it)) // []
console.log('hello' in it); // true
console.log(typeof it.hello); // function

Who ever said that JavaScript was rational? This is one of the reasons why TypeScript is a structurally typed language. In most ways it would be used, hello() is part of its structure.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants