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

Omit helper loses type information when used with extended Records. #36981

Open
charrondev opened this issue Feb 24, 2020 · 8 comments
Open

Omit helper loses type information when used with extended Records. #36981

charrondev opened this issue Feb 24, 2020 · 8 comments
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@charrondev
Copy link

charrondev commented Feb 24, 2020

TypeScript Version: 3.8 & Nightly

Search Terms:

  • Omit
  • Record
  • Any
  • Loses type information

Expected behavior:

I expect the following types to be as described in the inline comments.

type AnyRecord = Record<string, any>;
interface ExtendsAny extends AnyRecord {
   myKey1: string;
   myKey2: string;
}
// ExtendsAny['myKey1'] === string
// ExtendsAny['otherKey'] === any

type OmitsKey = Omit<ExtendsAny, "myKey2">;
// OmitsKey['myKey'] === string
// OmitsKey['otherKey'] === any

Please note that this works without the omit, or without the record, but not when both are used together.

Actual behavior:

What actually happens is that after using omit, all keys become "any" and any information for code completion, etc that the specifically defined keys even exist, are removed, even thought they were working in the original extension.

type AnyRecord = Record<string, any>;
interface ExtendsAny extends AnyRecord {
   myKey1: string;
   myKey2: string;
}
// ExtendsAny['myKey1'] === string
// ExtendsAny['otherKey'] === any

type OmitsKey = Omit<ExtendsAny, "myKey2">;
// 🚨Problem here 🚨
// OmitsKey['myKey1'] === any
// OmitsKey['otherKey'] === any


const instance: OmitsKey = {
    myKey1: "test",
}
// 🚨 When typing this no autocomplete is provided for myKey1 🚨
instance.myKey1.

Related Issues:

Potential Workaround

In the meantime, I am redefining the base interface (very large) because it comes from a library without the record type mixed in. I do my omit, then add it back in afterwards.

Code

/**
 * @copyright 2020 Adam (charrondev) Charron
 * @license MIT
 */

export interface IBase<T = number> {
    baseProp: T;
    baseProp2: T;
    baseFunction: () => T;
}

const base: IBase<number> = {
    baseProp: 0,
    baseProp2: 0,
    baseFunction: () => 0,
};

// number
base.baseProp;

///
/// Without the omit
///
type AnyRecord = Record<string, any>;

interface IExtend<T> extends IBase<T>, AnyRecord {
    extendProp: T;
}

const extended: IExtend<number> = {
    baseProp: 0,
    baseProp2: 0,
    baseFunction: () => 0,
    extendProp: 0,
};

// number
extended.baseProp2;

///
/// With an Omit
///
interface IOmittedInterface extends Omit<IExtend<number>, "baseProp"> {}

const omitted: IOmittedInterface = {
    baseProp2: 0,
    baseFunction: () => 0,
};

// any
omitted.baseProp2;
Output
/**
 * @copyright 2020 Adam (charrondev) Charron
 * @license MIT
 */
const base = {
    baseProp: 0,
    baseProp2: 0,
    baseFunction: () => 0,
};
// number
base.baseProp;
const extended = {
    baseProp: 0,
    baseProp2: 0,
    baseFunction: () => 0,
    extendProp: 0,
};
// number
extended.baseProp2;
const omitted = {
    baseProp2: 0,
    baseFunction: () => 0,
};
// any
omitted.baseProp2;
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 More Info The issue still hasn't been fully clarified label Mar 10, 2020
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 10, 2020

type AnyRecord = Record<string, any>;
interface ExtendsAny extends AnyRecord {
   myKey1: string;
   myKey2: string;
}
// ExtendsAny['myKey'] === string
// ExtendsAny['otherKey'] === any

type OmitsKey = Omit<ExtendsAny, "myKey2">;
// 🚨Problem here 🚨
// OmitsKey['myKey'] === any
// OmitsKey['otherKey'] === any

Maybe I'm just being dense here, but you declared two properties myKey1 and myKey2, then referenced the non-existent key myKey and expected different behavior from the other non-existent key otherKey ? I see expected behavior when I refer to myKey1 in both cases.

@charrondev
Copy link
Author

charrondev commented Mar 10, 2020

@RyanCavanaugh Sorry it looks my first example has some typos. The second one (that was linked from the code sandbox was correct.)

I've updated my example above and will out it here as well.

type AnyRecord = Record<string, any>;
interface ExtendsAny extends AnyRecord {
   myKey1: string;
   myKey2: string;
}
// ExtendsAny['myKey1'] === string
// ExtendsAny['otherKey'] === any

type OmitsKey = Omit<ExtendsAny, "myKey2">;
// 🚨Problem here 🚨
// OmitsKey['myKey1'] === any
// OmitsKey['otherKey'] === any

The problem is that myKey1 which was present as a fully-defined key in the original interface, loses it's type information when Omit is used on it.

@RyanCavanaugh RyanCavanaugh added Needs Investigation This issue needs a team member to investigate its status. and removed Needs More Info The issue still hasn't been fully clarified labels Mar 10, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.0 milestone Mar 10, 2020
@eugene-korobko
Copy link

I have similar problem

interface S1 {
    x: number;
    z: boolean;
    value?: string;
}

interface S2 {
    y: number;
    z: boolean;
    value?: string;
}

export interface TypesMap {
    s1: S1;
    s2: S2;
}

type Type = keyof TypesMap;

type Tp2<Tp extends Type = Type> = Omit<TypesMap[Tp], 'z'>;

class C<TP extends Type> {
    public f(v: Tp2<TP>): void {
        if (v.value !== undefined) {
            this.g(v.value);
        }
    }

    public g(value: string): void {

    }
}

Line this.g(v.value); generates an error

Argument of type 'TypesMap[TP]["value"]' is not assignable to parameter of type 'string'.
  Type 'string | undefined' is not assignable to type 'string'.
    Type 'undefined' is not assignable to type 'string'.(2345)

I believe the code is corrent

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Aug 31, 2020
@zlanich
Copy link

zlanich commented Oct 17, 2020

I have this exact problem, and it's kept from compiler from passing for 3 days now. I just realized it's because my top interface extends Record<string, any> (TSC: 3.7.2):

interface Item extends Record<string, any> { // problem starts here
  id: string
  parentId: string
}

interface SubItem extends Item {
  foo: string
}

// Omit causes ItemWithOnlyUserAssignableProps to lose its typing for "foo"
interface ItemWithOnlyUserAssignableProps extends Omit<SubItem, 'id' | 'parentId'> {}

// This does not error, and nothing down the line has type-suggestion support in VSCode anymore
const userAssignableItemProps: ItemWithOnlyUserAssignableProps = {
  // Missing "foo"
}

@Dan503
Copy link

Dan503 commented Aug 17, 2022

This is such a majorly painful issue.

I have this problem

(note: in this simple example the problem doesn't occur if it is all laid out in the one file. When importing the types, that is when the types can get lost, possibly only from an npm registry)

interface One {
  a: string | number
  b: string
  z: string
}

interface Two {
  a: string,
  b: string
}

// Error! Named property 'a' of types One and Two are not identical
interface Three extends One, Two {
  c: string
}

// So I do this to work around that problem...
interface Three extends Omit<One, 'a'>, Two {
  c: string
}

// but then...
const x = (params: Three) => {
  params.z // params.z is of the "unknown" type, not of type "string" like it is supposed to be
}

@Dan503
Copy link

Dan503 commented Oct 24, 2022

Ok I finally figured out the real problem behind why Omit was causing me so many headaches.

I have a type called UnknownProps that looks like this:

interface UnknownProps {
    [propName: string]: unknown
}

And I was extending my interfaces using it as a way of allowing extra props that were not strictly defined

interface ComponentProps extends UnknownProps  {
    value: string
    label: string
}

Then when I tried to combine ComponentProps with Omit I got no types as all

// FilteredProps is of type unknown
type FilteredProps = Omit<ComponentProps, 'label'>

Getting rid of extends UnknownProps fixed the issue for me (though I would prefer if Omit didn't hard fail and instead used the known keys that it had access to)

This is also a problem for keyof and probably a few other things. Might need to go under a separate issue...

@douglasg14b
Copy link

@Dan503 Did you figure out a solution that lets you have a type with a couple defined props, and allows any others? This is also being problematic for me as well.

@Dan503
Copy link

Dan503 commented Jan 12, 2023

@douglasg14b I ended up saving the known props into a static interface and then creating a seperate type for using on the component props.

export interface StaticComponentProps  {
    value: string
    label: string
}

export type ComponentProps = StaticComponentProps & UnknownProps

Then if I want to do something with the component props I target the staticly known StaticComponentProps interface instead of the dynamic ComponentProps type

type FilteredProps = Omit<StaticComponentProps, 'label'>
// FilteredProps = { value: string }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
9 participants