英文:
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<infer T>
, but leaves others untouched; something like this:
MyType<{ a: number, b: Model<string> }> // => { a: number, b: string }
I thought it would be as simple as this:
type MyType<T> = {
[P in keyof T]: T[P] extends Model<infer R> ? R : T[P];
};
but when testing it like this:
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
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<T> = {
[P in keyof T]: T[P] extends Model<infer R> ? R : T[P];
} & { [P in Exclude<keyof T, keyof Model<any>>]: 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<T> = {
[P in keyof T]: T[P] extends Model<infer R> ? R : T[P];
} & { [P in Exclude<keyof T, keyof Model<any>>]: 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]
的原因是因为所有类型都可以分配给{}
,除了null
和undefined
。基本上,
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]
是否是一个模型并推断出类型。
Filly的代码有效,本质上是在检查空对象是否可分配给字符串(或日期),这是无效的。这充当了在尝试推断模型的内部类型之前的保护。
type T = {} extends string ? true : false;
// ^? false
这意味着Model<unknown> extends T[P]
仅在T[P]
是空对象或Model<T>
时触发。
(如果您的Model类在结构上与{}
类型不同,则不需要Filly的解决方案。)
英文:
Since your Model class is empty, Model<T>
is the same as {}
(the empty object type). So when you use T[P] extends Model<infer R>
, 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<T>{
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<T>
, and TypeScript can now check if T[P]
is a model and infer the type.
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<unknown> extends T[P]
only triggers if T[P]
is an empty object or Model<T>
.
(you don't need Filly's solution if your Model class is structurally different from the {}
type)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论