TypeScript 当与扩展记录一起使用时,省略助手会丢失类型信息,

qq24tv8q  于 6个月前  发布在  TypeScript
关注(0)|答案(8)|浏览(48)

TypeScript 版本: 3.8 & Nightly
搜索词:

  • Omit
  • Record
  • Any
  • 丢失类型信息
    预期行为:

我希望以下类型按照内联注解描述。

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

请注意,在不使用 omit 或不使用 record 的情况下,这可以正常工作,但当两者一起使用时,实际行为如下。
在使用 omit 之后,所有键都变成了 "any",即使特定定义的键存在,用于代码补全等的任何信息也会被删除,尽管它们在原始扩展中可以正常工作。

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.

相关问题:

在此期间,我正在重新定义基本接口(非常大),因为它来自一个没有混合记录类型的库。我先执行 omit,然后再将其添加回来。

代码

/**
* @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;

输出

/**
* @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;

编译器选项

{
  "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 链接:**提供

iugsix8n

iugsix8n1#

也许我只是在这里显得有些愚钝,但是你声明了两个属性 myKey1myKey2 ,然后引用了不存在的键 myKey ,并期望从另一个不存在的键 otherKey 中得到不同的行为?在我提到 myKey1 的情况下,我看到了预期的行为。

s8vozzvw

s8vozzvw2#

对不起,我的第一条示例中有一些拼写错误。第二条(从代码沙箱链接的)是正确的。
我已经更新了上面的示例,并将其放在这里。

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

问题在于,myKey1 在原始接口中作为完全定义的键存在,但在使用Omit时会丢失其类型信息。

kknvjkwl

kknvjkwl3#

我遇到了类似的问题

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); 生成了一个错误

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)

我相信代码是正确的

rkue9o1l

rkue9o1l4#

我遇到了这个问题,编译器已经阻止它通过3天了。我刚刚意识到这是因为我的顶层接口扩展了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"
}
klr1opcd

klr1opcd5#

这是一个非常痛苦的问题。
我遇到了这个问题(注意:在这个简单的示例中,如果所有内容都放在一个文件中,问题就不会发生。当导入类型时,可能会丢失类型,可能只从npm注册表中丢失)。

vom3gejh

vom3gejh6#

好的,我终于找到了Omit给我带来这么多麻烦的真正原因。
我有一个类型叫做UnknownProps,它看起来是这样的:

interface UnknownProps {
    [propName: string]: unknown
}

我通过它来扩展我的接口,作为一种允许额外属性的方式,这些属性并没有严格定义

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

然后当我尝试将ComponentProps与Omit结合时,我没有得到任何类型,因为所有的

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

摆脱extends UnknownProps解决了我的问题(尽管我更希望Omit不要硬性失败,而是使用它已经访问到的已知键)
这也是keyof和其他一些东西的问题。可能需要单独开一个问题...

6tqwzwtp

6tqwzwtp7#

@Dan503 你有没有找到一个解决方案,让你可以定义一些属性的类型,并且允许其他类型?这对我来说也是一个问题。

wkftcu5l

wkftcu5l8#

我最终将已知的属性保存到一个静态接口中,然后为组件属性创建一个单独的类型。

export interface StaticComponentProps  {
    value: string
    label: string
}

export type ComponentProps = StaticComponentProps & UnknownProps

接下来,如果我想要对组件属性执行某些操作,我会针对静态已知的 StaticComponentProps 接口而不是动态的 ComponentProps 类型进行操作。

相关问题