Skip to content
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

Filter object properties by type #38646

Closed
turolla opened this issue May 18, 2020 · 12 comments
Closed

Filter object properties by type #38646

turolla opened this issue May 18, 2020 · 12 comments
Labels
Duplicate An existing issue was already created

Comments

@turolla
Copy link

turolla commented May 18, 2020

TypeScript Version: 3.9.2

Search Terms:
mapped type filter object properties never

Expected behavior:
The type
type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];
that allows to extract object properties by type should work everywhere.

Actual behavior:
type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];
only works externally of the declaring type

Related Issues:
#23199

Code

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];

type FooItem<T> = T extends number ? { value: T } : undefined;

type FooMap<T> = {
    // [key in keyof T]: FooItem<T[key]>  // <-- This works
    [key in FilteredKeys<T, number>]: FooItem<T[key]>  // <-- This doesn't work
}

class Bar {
    a = 1;
    b = 2;
    c = '';    

    doSomething() {
        const fooMap: FooMap<this> = undefined!; // Omitted...
        const a = fooMap.a.value; // <-- No error expected
        const c = fooMap.c.value; // <-- Error expected
    }
}

const fooMap2: FooMap<Bar> = undefined!; // Omitted...
const a = fooMap2.a.value; // <-- No error expected
const c = fooMap2.c.value; // <-- Error expected
Output
"use strict";
class Bar {
    constructor() {
        this.a = 1;
        this.b = 2;
        this.c = '';
    }
    doSomething() {
        const fooMap = undefined; // Omitted...
        const a = fooMap.a.value; // <-- No error expected
        const c = fooMap.c.value; // <-- Error expected
    }
}
const fooMap2 = undefined; // Omitted...
const a = fooMap2.a.value; // <-- No error expected
const c = fooMap2.c.value; // <-- Error expected
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jun 8, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.0 milestone Jun 8, 2020
@RyanCavanaugh
Copy link
Member

@weswigham thoughts?

@dumasss163
Copy link

dumasss163 commented Aug 7, 2020

you could do this to make it compile:

type FilteredKeys<T, U> = keyof { [P in keyof T]: T[P] extends U ? P : never };

But this still has other problem sometimes, see:
Property Type inference problem when picking property with index key type · Issue #39945 · microsoft/TypeScript

@shauns
Copy link

shauns commented Sep 29, 2020

In 4.1 (beta) you can do something like:

type PickByValueType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}

@alvis
Copy link

alvis commented Oct 26, 2020

In 4.1, as @shauns mentioned, you can filter out object keys which don't fit the type you want. However, unfortunately, it still can't give you a list of keys which comply with the desired type by the conventional key of trick. i.e. The following doesn't work:

type SelectKeysByValueType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}[keyof T] // <~ Type 'keyof T' cannot be used to index type

See also #40833

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.6.0 milestone Dec 15, 2021
@pascalgn
Copy link

As a workaround, I think this could work:

type OriginalType = {
    str: string;
    num: number;
}

type Values<T> = T[keyof T];

type PickKeys<T, U> = Values<{
  [K in keyof T]: T[K] extends U ? K : never;
}>;

type NumberKeys = PickKeys<OriginalType, number>;
type NumberType = Pick<OriginalType, NumberKeys>;

const ok: NumberType = { num: 1 }; // ok
const err: NumberType = { num: 1, str: "" }; // error

With the original example rewritten:

type FooMap<T> = {
    [key in PickKeys<T, number>]: T[key]
}

const ok1: FooMap<OriginalType> = { num: 1 }; // ok
const err2: FooMap<OriginalType> = { num: 1, str: "" }; // error

@aspic-fish
Copy link

I have a workaround, just infer type before mapping it.
So rewrite this:

type FooMap<T> = {
    [key in FilteredKeys<T, number>]: FooItem<T[key]> 
}

to this:

type FooMap<T> = T extends infer TT 
  ?  {
    [key in FilteredKeys<T, number>]: FooItem<T[key]>
  }
  : never

turolla case

ziofat case

@turolla
Copy link
Author

turolla commented Feb 28, 2022

I have a workaround, just infer type before mapping it. So rewrite this:

type FooMap<T> = {
    [key in FilteredKeys<T, number>]: FooItem<T[key]> 
}

to this:

type FooMap<T> = T extends infer TT 
  ?  {
    [key in FilteredKeys<T, number>]: FooItem<T[key]>
  }
  : never

turolla case

ziofat case

@aspic-fish YOU ARE A GENIUS !!!! Thank you so much!!!

@edvald
Copy link

edvald commented Jul 11, 2022

Here's a solution we found to work for us (in this case omitting as opposed to picking), including a recursive option:

type OmitKeysByValueType<T, U> = { [P in keyof T]: T[P] extends U ? never : P }[keyof T]

type OmitByValueType<T, V> = T extends infer _
  ? {
      [key in OmitKeysByValueType<T, V>]: T[key]
    }
  : never

type OmitByValueTypeRecursive<T, V> = T extends object
  ? T extends infer _
    ? {
        [key in OmitKeysByValueType<T, V>]: OmitByValueTypeRecursive<T[key], V>
      }
    : never
  : T

Usage:

interface A {
  someFunctionIDoNotWant: () => void // <- get rid of this
  somePropIDoWant: number
}

interface B {
  prop: string
  prop2: A
}

type AMinusFunctions = OmitByValueType<A, Function>
// -> { somePropIDoWant: true }

type BMinusFunctionsRecursively = OmitByValueTypeRecursive<B, Function>
// -> { prop: string; prop2: {  somePropIDoWant: true } }

cc @Orzelius

@RyanCavanaugh
Copy link
Member

A few observations:

  • The nonworking form isn't something we can analyze for an arbitrary type parameter like this and get the right answer in the general case
  • The working form... works?
  • Looks like there are other workarounds too

The real missing feature is #48992 to handle this in a more analyzable form, so I think we can treat this as a duplicate of that one

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone labels Feb 15, 2023
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 5.1.0 milestone Feb 15, 2023
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests