Typescript: 特定泛型类型的类型解包属性

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

Typescript: Type unwrapping properties of specific generic type

问题

我想实现一个类型,可以提取所有属性的内部类型,这些属性是Model<infer T>,但保持其他属性不变,类似于这样:

MyType<{ a: number, b: Model<string> }> // => { a: number, b: string }

我以为这很简单:

type MyType<T> = {
  [P in keyof T]: T[P] extends Model<infer R> ? R : T[P];
};

但当像这样测试它时:

const fnForTesting = <T>(obj: T): MyType<T> => {
  return null!;
};

const result = fnForTesting({
  name: "Ann",
  age: new Model<number>(),
});

const a = result.name; // unknown :( - should be string
const b = result.age; // number - works as expected

有人知道为什么“正常”属性没有被正确识别,而Model属性是吗?我该如何修复这个问题?先感谢你!

英文:

I want to achieve a type that extracts the inner-type of all properties being a Model&lt;infer T&gt;, but leaves others untouched; something like this:

MyType&lt;{ a: number, b: Model&lt;string&gt; }&gt; // =&gt; { a: number, b: string }

I thought it would be as simple as this:

type MyType&lt;T&gt; = {
  [P in keyof T]: T[P] extends Model&lt;infer R&gt; ? R : T[P];
};

but when testing it like this:

const fnForTesting = &lt;T&gt;(obj: T): MyType&lt;T&gt; =&gt; {
  return null!;
};

const result = fnForTesting({
  name: &quot;Ann&quot;,
  age: new Model&lt;number&gt;(),
});

const a = result.name; // unknown :( -&gt; should be string
const b = result.age; // number -&gt; works as expected

Does anybody know why "normal" properties are not recognized correctly, while the Model properties are? And how do I fix this? Thanks in advance!

答案1

得分: 2

你的实现仅仅检查属性类型是否扩展了Model<infer R>,如果没有,它将简单地返回原始类型而没有任何修改。

要解决这个问题,你可以更新MyType类型,包括一个用于任何不扩展Model<infer R>属性的原始类型的通用情况:

type MyType&lt;T&gt; = {
  [P in keyof T]: T[P] extends Model&lt;infer R&gt; ? R : T[P];
} &amp; { [P in Exclude&lt;keyof T, keyof Model&lt;any&gt;&gt;]: T[P] };

这个更新后的实现添加了第二个映射类型,其中包括所有不是Model<infer R>的属性。使用Exclude<keyof T, keyof Model<any>>表达式来获取不属于Model<any>的键。使用&运算符来组合这两个映射类型。

英文:

Your implementation only checks if the property type extends Model<infer R>, but if it does not, it simply returns the original type without any modifications.

To fix this, you can update the MyType type to include a catch-all case that returns the original type for any properties that do not extend Model<infer R>:

type MyType&lt;T&gt; = {
  [P in keyof T]: T[P] extends Model&lt;infer R&gt; ? R : T[P];
} &amp; { [P in Exclude&lt;keyof T, keyof Model&lt;any&gt;&gt;]: T[P] };

This updated implementation adds a second mapped type that includes all properties that are not a Model<infer R>. The Exclude<keyof T, keyof Model<any>> expression is used to get the keys that are not a part of Model<any>. The & operator is used to combine the two mapped types.

答案2

得分: 2

由于您的Model类为空,Model<T>{}(空对象类型)相同。因此,当您使用T[P] extends Model<infer R>时,TypeScript无法正确地推断R,所以它使用unknown。 TypeScript不会退而回退到T[P]的原因是因为所有类型都可以分配给{},除了nullundefined。基本上,

type T = string extends {} ? true : false;
//   ^? true

现在请注意,当我向您的Model类添加属性以使其非空时,您的原始代码可以正常工作:

class Model<T> {
    value!: T;

    constructor() { }
}

// ...

const a = result.name; // string
//    ^?
const b = result.age; // number 
//    ^?
const c = result.date; // date 
//    ^?

这是因为现在字符串(或日期)与Model<T>之间有明显的结构差异,TypeScript现在可以检查T[P]是否是一个模型并推断出类型。

Playground(更改后的Model)


Filly的代码有效,本质上是在检查空对象是否可分配给字符串(或日期),这是无效的。这充当了在尝试推断模型的内部类型之前的保护。

type T = {} extends string ? true : false;
//   ^? false

这意味着Model<unknown> extends T[P]仅在T[P]是空对象或Model<T>时触发。

Playground(Filly的解决方案)

(如果您的Model类在结构上与{}类型不同,则不需要Filly的解决方案。)

英文:

Since your Model class is empty, Model&lt;T&gt; is the same as {} (the empty object type). So when you use T[P] extends Model&lt;infer R&gt;, TypeScript cannot infer R properly, so it uses unknown. The reason why TypeScript doesn't back off and fallback to T[P] is because all types are assignable to {} except for null and undefined. Basically,

type T = string extends {} ? true : false;
//   ^? true

Now notice that when I add a property to your Model class, to make it non-empty, your original code works:

class Model&lt;T&gt;{
    value!: T;

    constructor() { }
}

// ...

const a = result.name; // string
//    ^?
const b = result.age; // number 
//    ^?
const c = result.date; // date 
//    ^?

This is because there is now a clear, structural difference between a string (or date) and a Model&lt;T&gt;, and TypeScript can now check if T[P] is a model and infer the type.

Playground (changed Model)


Filly's code works you're essentially checking if an empty object is assignable to a string (or date), which is invalid. This acts as a guard before trying to infer the inner type of the model.

type T = {} extends string ? true : false;
//   ^? false

That means Model&lt;unknown&gt; extends T[P] only triggers if T[P] is an empty object or Model&lt;T&gt;.

Playground (Filly's solution)

(you don't need Filly's solution if your Model class is structurally different from the {} type)

huangapple
  • 本文由 发表于 2023年3月3日 20:43:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/75627227.html
匿名

发表评论

匿名网友

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

确定