Typescript与泛型冲突

huangapple go评论105阅读模式
英文:

Typescript conflict with Generics

问题

我正在尝试在以下示例中使用通用类型:

export interface CommonFilter<T> {
    value?: T,
    renderValue: (val?: T) => string
}

export interface StringFilter extends CommonFilter<string> {
    type: "string",
    customStringAttr?: any
}

export interface NumberFilter extends CommonFilter<number> {
    type: "number",
    customNumberAttr?: any
}

export type Filter = StringFilter | NumberFilter;

到目前为止,一切正常。

问题出在我想要使用每个过滤器的renderValue函数时:

const filters: Filter[] = [
  { type: "string", value: "something", renderValue: val => `My string val --> ${val ?? "none"}` },
  { type: "number", value: undefined, renderValue: val => `My number val --> ${val?.toFixed(2) ?? "none"}` }
];

filters.forEach(f => {
  f.renderValue(f.value); // 这一行失败
})

我收到的错误信息是:

参数类型 'string | number | undefined' 不能分配给类型 'undefined' 的参数。
类型 'string' 不能分配给类型 'undefined'。

有人有解决方法吗?

英文:

I am trying to use generic types in the following example:

export interface CommonFilter&lt;T&gt; {
    value?: T,
    renderValue: (val?: T) =&gt; string
}

export interface StringFilter extends CommonFilter&lt;string&gt; {
    type: &quot;string&quot;,
    customStringAttr?: any
}

export interface NumberFilter extends CommonFilter&lt;number&gt; {
    type: &quot;number&quot;,
    customNumberAttr?: any
}

export type Filter = StringFilter | NumberFilter;

So far, all good.

The problem comes when I want to use the renderValue of each filter:

const filters: Filter[] = [
  { type: &quot;string&quot;, value: &quot;something&quot;, renderValue: val =&gt; `My string val --&gt; ${val ?? &quot;none&quot;}` },
  { type: &quot;number&quot;, value: undefined, renderValue: val =&gt; `My number val --&gt; ${val?.toFixed(2) ?? &quot;none&quot;}` }
];

filters.forEach(f =&gt; {
  f.renderValue(f.value); //this line fails 
})

The error that I get is:
> Argument of type 'string | number | undefined' is not assignable to parameter of type 'undefined'.
Type 'string' is not assignable to type 'undefined'.

Does anyone have an idea how to fix it?

答案1

得分: 1

因为 renderValuevalue 是可选属性,意味着类型的这些字段是 T | undefined

export interface CommonFilter<T> {
  value?: T, // 真实类型:T | undefined
  renderValue?: (val?: T) => string // 真实类型 ((val?: T) => string) | undefined
}

所以,在调用可能是未定义的函数时,会出现 TypeScript 错误。您可以在调用 renderValue 之前尝试检查是否为 undefined。对于 value 字段的真实类型是 string | number | undefined,您可以将其转换为 any

filters.forEach(f => {
  if (!f.renderValue) return;
  console.log(f.renderValue(f.value as unknown as any)); // val 参数的真实类型是 number | string | undefined。
})
英文:

Because renderValue and value is optional properties that mean type's those fields is T | undefined

export interface CommonFilter&lt;T&gt; {
  value?: T, // real type: T | undefined
  renderValue?: (val?: T) =&gt; string // real type ((val?: T) =&gt; string) | undefined
}

So, when call possible undefined function will occur typescript error. You can try check undefined before call renderValue. With value field real type is string | number | undefined, you can cast to any.

filters.forEach(f =&gt; {
  if (!f.renderValue) return;
  console.log(f.renderValue(f.value as unknown as any)); // real type of val param is number | string | undefined.
})

答案2

得分: 1

请检查f.type以缩小f.value的类型:

filters.forEach(f => {
  if (f.renderValue) {
    if (f.type == "number") {
      f.renderValue(f.value);  
    }
    else {
      f.renderValue(f.value);
    }
  }
})

或者简单地将renderValue中的值类型更改为unknown,然后检查是否存在renderValue

renderValue?: (val?: unknown) => string
filters.forEach(f => {
  if (f.renderValue) {
    f.renderValue(f.value);
  }
})
英文:

Either check the f.type to narrowing the type of the f.value:

filters.forEach(f =&gt; {
  if (f.renderValue) {
    if (f.type == &quot;number&quot;) {
      f.renderValue(f.value);  
    }
    else {
      f.renderValue(f.value);
    }
  }

})

or simply change the value type in renderValue to unkown and check the renderValue if exist:

 renderValue?: (val?: unknown) =&gt; string
filters.forEach(f =&gt; {
if(f.renderValue){
    f.renderValue(f.value); 
}
})

答案3

得分: 1

编译器无法处理在ms/TS#30581中描述的"相关联联合类型",而不是使用断言来破坏类型安全性,或者为每种过滤器类型使用开关案例,让我们使用ms/TS#47109中描述的建议方法。

首先,我们需要一个类型,用于存储所有过滤器所需的类型:

type TypeMap = {
  string: {
    type: string;
    extra: { customStringAttr?: any };
  };
  number: {
    type: number;
    extra: { customStringAttr?: any };
  };
};

extra字段用于添加实际需要的一些额外字段。

现在,让我们使用映射类型重新创建您的过滤器类型:

type FilterObject = {
  [K in keyof TypeMap]: {
    type: K;
    value?: TypeMap[K]['type'];
    renderValue?: (val?: TypeMap[K]['type']) => string;
  } & TypeMap[K]['extra'];
};

测试:

type FilterObject = {
    string: {
        type: "string";
        value?: string | undefined;
        renderValue?: ((val?: string | undefined) => string) | undefined;
    } & {
        customStringAttr?: any;
    };
    number: {
        type: "number";
        value?: number | undefined;
        renderValue?: ((val?: number | undefined) => string) | undefined;
    } & {
        ...;
    };
}

要实际获取过滤器数组,我们将使用此答案中描述的ValueOf类型:

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

// (({
//   type: "string";
//   value?: string | undefined;
//   renderValue?: ((val?: string | undefined) => string) | undefined;
// } & {
//   customStringAttr?: any;
// }) | ({
//   type: "number";
//   value?: number | undefined;
//   renderValue?: ((val?: number | undefined) => string) | undefined;
// } & {
//   ...;
// }))[]
type Result = ValueOf<FilterObject>[]

const filters: ValueOf<FilterObject>[] = [
  {
    type: 'string',
    value: 'something',
    renderValue: (val) => `My string val --> ${val ?? 'none'}`,
  },
  {
    type: 'number',
    value: undefined,
    renderValue: (val) => `My number val --> ${val?.toFixed(2) ?? 'none'}`,
  },
];

要完成这个方法,我们需要一个通用函数,它将接受一个受约束的泛型参数,该参数受限于keyof TypeMap,并且参数将是该键下的整个过滤器:

const render = <T extends keyof TypeMap>(arg: FilterObject[T]) => {
  arg.renderValue?.(arg.value);
};

用法:

filters.forEach((f) => {
  render(f); // 没有错误
});

playground

英文:

The compiler is unable to handle the "correlated union types" described in ms/TS#30581,
Instead of using assertion, which breaks the type-safety, or having a switch case for every type of filter, let's use the suggested approach described in ms/TS#47109.

First, we will need a type that would store all necessary type for filter:

type TypeMap = {
  string: {
    type: string;
    extra: { customStringAttr?: any };
  };
  number: {
    type: number;
    extra: { customStringAttr?: any };
  };
};

extra field is used to add some additional fields that you actually need.

Now, let's recreate your filter types using mapped types:


type FilterObject = {
  [K in keyof TypeMap]: {
    type: K;
    value?: TypeMap[K][&#39;type&#39;];
    renderValue?: (val?: TypeMap[K][&#39;type&#39;]) =&gt; string;
  } &amp; TypeMap[K][&#39;extra&#39;];
};

Testing:

type FilterObject = {
    string: {
        type: &quot;string&quot;;
        value?: string | undefined;
        renderValue?: ((val?: string | undefined) =&gt; string) | undefined;
    } &amp; {
        customStringAttr?: any;
    };
    number: {
        type: &quot;number&quot;;
        value?: number | undefined;
        renderValue?: ((val?: number | undefined) =&gt; string) | undefined;
    } &amp; {
        ...;
    };
}

To actually get the array of filters we will use the ValueOf described in this answer:

type ValueOf&lt;T&gt; = T[keyof T];

// (({
//   type: &quot;string&quot;;
//   value?: string | undefined;
//   renderValue?: ((val?: string | undefined) =&gt; string) | undefined;
// } &amp; {
//   customStringAttr?: any;
// }) | ({
//   type: &quot;number&quot;;
//   value?: number | undefined;
//   renderValue?: ((val?: number | undefined) =&gt; string) | undefined;
// } &amp; {
//   ...;
// }))[]
type Result = ValueOf&lt;FilterObject&gt;[]

const filters: ValueOf&lt;FilterObject&gt;[] = [
  {
    type: &#39;string&#39;,
    value: &#39;something&#39;,
    renderValue: (val) =&gt; `My string val --&gt; ${val ?? &#39;none&#39;}`,
  },
  {
    type: &#39;number&#39;,
    value: undefined,
    renderValue: (val) =&gt; `My number val --&gt; ${val?.toFixed(2) ?? &#39;none&#39;}`,
  },
];

For finishing the approach we will need a generic function that will accept a generic parameter constrained by keyof TypeMap and the argument will be the whole filter under that key:

const render = &lt;T extends keyof TypeMap&gt;(arg: FilterObject[T]) =&gt; {
  arg.renderValue?.(arg.value);
};

Usage:

filters.forEach((f) =&gt; {
  render(f); // no error
});

playground

huangapple
  • 本文由 发表于 2023年7月20日 18:48:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76729107.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定