awesome-typescript
awesome-typescript copied to clipboard
「重学TS 2.0 」TS 练习题第一题
第一题
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
以上代码为什么会提示错误,应该如何解决上述问题?
请在下面评论你的答案
// 1
type User = {
id: number;
kind: string;
}
function makeCustomer<T extends User>(u: T): T {
return {
// ...u,
id: u.id,
kind: 'customer'
}
}
makeCustomer({ id: 1, kind: '张三', name: '李四' })
看了答案后来回答的
关键在extends上,如果不用扩展运算符进行返回,并后续覆盖属性的话,函数接收的参数如上述缩写,就会有问题
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
回答:T 类型兼容 User类型,
第一种回答:
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
...u,
id: u.id,
kind: 'customer',
};
}
第二种返回值限制为User 类型的
function makeCustomer<T extends User>(u: T): ReturnMake<T, User> {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer',
};
}
type ReturnMake<T extends User, U> = {
[K in keyof U as K extends keyof T ? K : never]: U[K];
};
为什么报错?
- 因为 T 只是约束与 User 类型,而不局限于User 类型,所以返回为T类型不仅仅只有 id和kind,So需要一个接收其他类型的变量
解决方案:
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
返回类型 让其自动推导
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T) {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
};
}
@semlinker 这个可以解释下吗?不太理解
可以直接重新定义返回数据类型 type User = { id: number; kind: string; };
function makeCustomer<T extends User>(u: T): User { return { id: u.id, kind: 'customer' } }
type User = {
id: number;
kind: string;
};
// 第一种解法是修改函数返回 的类型 为 User 类型
function makeCustomer<T extends User>(u: T): User {
return {
id: u.id,
kind: 'customer'
}
}
// 第二种是解法是 修改函数返回类型为User类型的子类型
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
function makeSuperUser<T extends User>(t: T): T {
return {
id: t.id,
name: 'customer'
} as T // 添加as 类型断言
}
其实是需要知道makeCustomer想要返回什么,因为类型其实也是可以继承的,这就代表T可能会提供更多的属性或方法。比如我有一个继承了User类型的MyUser类型,它是能够作为参数传入的,T也就变成了MyUser,而现在的makeCustomer只是返回了User类型。
interface MyUser extends User {
age: number
}
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
// 提供两种解法
// 1.这种实际返回的是 T 类型
// return {
// ...u,
// id: u.id,
// kind: 'customer',
// }
// 2.这种还是返回的 User 类型,但是内部强制断言了
return {
id: u.id,
kind: 'customer',
} as T
}
declare const myUser: MyUser
makeCustomer(myUser)
type User = {
id: number;
kind: string;
};
type User = {
id: number;
kind: string;
};
// 第一种
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
// 第二种
function makeCustomer<T extends User>(u: T): U {
return {
id: u.id,
kind: 'customer'
}
}
因为返回值要求是T的类型,但是返回了User类型;而T类型是User的子类型,可以赋值给User,但是User不能赋值给T
第一种方案
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): User {
return {
id: u.id,
kind: 'customer'
}
}
第二种方案
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
...u, // 缺少这一样的话,返回的类型是 User,而非 T,T 是 User 的子类型,约束条件更多,子类可以赋值给父类,反过来不行
id: u.id,
kind: 'customer'
};
}
解题思路如注释
type User = {
id: number;
kind: string;
};
type MyUser extends User{
age:number
}
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
};
}
let obj:MyUser = {id:1, kind:'foo', age:24}
makeCustomer<MyUser>(obj)
报错原因
- 泛型
T由于是在调用该函数的时候才能确定,T类型有可能会存在别的类型,因此返回值{id: u.id, kind: 'customer'}, 不一定符合泛型T。 - 例如 上述列子:
MyUser的类型符合User类型的约束,但是还存在age类型,显然函数的返回值,并不满足MyUser类型。
解决办法
- 让返回值满足泛型
T -
function makeCustomer<T extends User>(u: T): T { return { ...u, id: u.id, kind: 'customer' }; } - 在不确定泛型
T的情况下不推荐类型断言。
@semlinker 这个可以解释下吗?不太理解
T extends User 的意思 约束泛型T 符合 User结构,但不局限于这个结构。 如果我makeCustomer({ id: 1, kind: '2', age: 30 });
那么泛型T自动推导为 { id: number; kind: string; age: number } 这样就满足了User的约束 可以入参。但是返回的类型也限定成了这个结构。 那么例子中 的返回 return { id: u.id, kind: 'customer' } 就不满足于{ id: number; kind: string; age: number } 因为少了一个age。
这其实就是一个类型兼容的问题,举个例子: class Person{ name: string; age: number;
}
class Cat{ name: string; age: number; cici:true } //这样是可以的 这相当于Cat extends Person function fn(p:Person){ p.name } fn(new Cat())
//这样是不可以的 // function fn(c:Cat){ // c.name // } // fn(new Person())
所以这个问题的解决办法是:改返回值的类型,或者改返回值 type user = { id:number, kind:string }
function makeCostomer<T extends user>(u:T):T{ return { ...u, id:1, kind:'dd', } }
或者
type user = { id:number, kind:string }
function makeCostomer<T extends user>(u:T):user{ return { // ...u, id:1, kind:'dd', } } 以后如果想明白更好的写法再来补充
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
// T 的范围 大于 {id:number,kind:string}
...u,
id: u.id,
kind: "customer",
};
}
interface Mytype extends User {
myName: string;
}
const demo: Mytype = { id: 1, kind: "demo", myName: "lwt" };
console.log(makeCustomer<Mytype>(demo));
export {};
上面用 ... 扩展运算符的,例如传入参数 makeCustomer({ id: 1, kind: 'yes', name: 'qiuxc' }) 会返回 { id: 1, kind: 'yes', name: 'qiuxc' },而题意只想返回 id 和 kind
可以使用 Pick 来筛选出返回值的类型:
type User = {
id: number
kind: string
}
function makeCustomer<T extends User>(u: T): Pick<T, 'id' | 'kind'> {
return {
id: u.id,
kind: 'customer'
}
}
console.log(makeCustomer({ id: 1, kind: 'yes', name: 'qiuxc' }))
// { id: 1, kind: 'yes' }
type User = { id: number; kind: string; };
// --------------------------- 错误 ---------------------------------- // extends 从语义上看是继承的意思,意思是包含兼容,所以T中包含属性<id, kind>, 但是不一定只有这两个 function makeCustomer<T extends User>(u: T): T { return { id: u.id, kind: 'customer' } }
// -------------------------- 正确 -------------------------------------- // 方式1 function makeCustomer1<T extends User>(u: T): User { return { id: u.id, kind: 'customer' } }
// 方法2 function makeCustomer2<T extends User>(u: T): T { return { ...u, id: u.id, kind: 'customer' } }
// 方法3 function makeCustomer3<T extends User>(u: T) { return { id: u.id, kind: 'customer' } }
type User = { id: number; kind: string; };
function makeCustomer<T extends User>(u: T): T { return { ...u, id: u.id, kind: u.kind } }
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
问题分析
根源还是在TS类型的 可赋值性 上,T 被约束为 User ,但是根据 可赋值性以下:
type A = {
id: number;
kind: string;
other: number;
};
A 也是可以赋值给 User ,此时 T 就为 A ,但是你仅仅返回 User,这明显是不安全的。
解决思路
修改函数返回
- 使用拓展运算符
Object.assign()
修改函数返回类型
Pick<T, 'id' | 'kind'> 等等,上面已有很多例子
https://juejin.cn/post/7062903623470514207 水了一篇文章,这里面写了48道题目的解法逻辑解析。😁
核心的关键是subtype,如何理解。
错误翻译:
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T', 返回的...可以附值给T的限制类型,即User // but 'T' could be instantiated with a different subtype of constraint 'User'. 但是,T可能在实例化的时候,会是一个不同的subtype(基于限制类型User)
白话说明:
你返回的object确实能够符合T的要求,就是此时的T确实是一个User的衍生子类型;但是,由于T会在函数调用的时候才能确定(这也是generics的设计初衷),调用的时候可能会是另外一个User的衍生自类型。由于此subtype跟你限定的subtype可能不一样,所以报错。
###subtype是啥,什么叫different subtype? subtype就是基于一个限制类型(如User)生成的type,这些不同的subtype都会在User上面添加不同的属性,如果添加的属性不同,则是different subtype,例子
type Foo = { readonly 0: '0'}
type SubType = { readonly 0: '0', readonly a: 'a'}
type DiffSubType = { readonly 0: '0', readonly b: 'b'}
const foo: Foo = { 0: '0'}
const foo_SubType: SubType = { 0: '0', a: 'a' }
const foo_DiffSubType: DiffSubType = { 0: '0', b: 'b' }
结论
对于限制类型的generics,小心注意不能做提前定死。比如这个例子也是同理:
const func1 = <T extends string>(a: T = 'foo') => `hello!` // Error!
const func2 = <T extends string>(a: T) => {
//stuff
a = `foo` // Error!
//stuff
}
扩展阅读
https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint
解决方案
思路:不能事先定死T。
- Pick的方法(见上)
- spread operator方法(见上)
- as断言方法(见上)
- 让TS自己去inference(见上)
- any方法(不好)
function makeCustomer<T extends User>(u: T): any {
return {
id: u.id,
kind: 'customer'
}
}
- 使用ReturnMake(见下)
--------------------------分割线----------------------- ###错误复显
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T) : T{
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
makeCustomer({
id: 2,
kind: 'good_customer',
city:'shangahi'
})
makeCustomer({
id: 2,
kind: 'bad_customer',
isPoor: true
})
关于ReturnMake,来源 https://juejin.cn/post/7062903623470514207
function makeCustomer<T extends User>(u: T): User {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer',
};
}
function makeCustomer<T extends User>(u: T): ReturnMake<T, User> {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer',
};
}
type ReturnMake<T extends User, U> = {
[K in keyof U as K extends keyof T ? K : never]: U[K];
};
makeCustomer({ id: 18584132, kind: '888', price: 99 });
类似题目:

解决方案:
const func1 = <T extends string> (a: T ) : ReturnMake<T, string> => `hello!` // Ok!
type ReturnMake<T extends string, U> = {
[K in keyof U as K extends keyof T ? K : never]: U[K];
};
/*
结论:返回值限制为T并非User对象
我们都知道函数有返回必定要指定返回类型,函数指定返回的类型是T,T 是 User 的子类型
这时函数返回一个对象而指定的返回类型为子类型,所以就报错了
*/
// 第一种指定返回类型T改为User
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): User {
return {
id: u.id,
kind: 'customer'
}
}
// 第二种 保留指定返回类型T
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
定义的返回值类型是泛型 T 函数返回的类型是 {id: number, kind: string} ,所以不匹配
<T extends User> 代表 泛型 T 包含 User 的属性且可以多于 User的属性
修改为
function makeCustomer<T extends User>(u: T): T { return { ...u, kind: 'customer' } }
type User = {
id: number,
kind: string
}
function makeCustome2r<T extends User>(u: T): Pick<T, keyof User> {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
返回的类型为T,T泛型约束符合User结构,但是并不局限于User结构,将其返回值类型Pick为User即可
type User = {
id: number;
kind: string;
};
// 返回T类型
function makeCustomer<T extends User>(u: T):T{
return {
...u,
id: u.id,
kind: 'customer'
}
}
返回的类型应该与u一致
原题代码
type User = {
id: number,
kind: string
}
function makeCustomer <T extends User>(u:T) :T {
return {
id: u.id,
kind: 'customer'
}
}
分析
原题中makeCustomer定义是一个函数,入参是一个类型T的形参u,返回值类型为继承了类型User的类型T(这一点意味着T的类属性至少要包含id和kind),但是该函数体实现却是返回一个仅仅分别含有id和kind两个属性的一个对象。
解答
1.可以看到函数体中的返回值其实符合类型User,也就是说我们可以定义函数的返回值类型为User,如下
type User = {
id: number,
kind: string
}
function makeCustomer <T extends User>(u:T) :User {
return {
id: u.id,
kind: 'customer'
}
}
2.按照原题思路,函数定义一定要返回一个类型为T的对象时,我们需要做的就是将函数体的实现中的返回值改为不仅仅只有id和kind两个属性的对象,如下:
type User = {
id: number,
kind: string
}
function makeCustomer <T extends User>(u:T) :T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
注意不要这样写,否则id&kind属性有可能将会被覆盖
{
id: u.id,
kind: 'customer',
...u,
}
3.该题为开放性题目,另外的的几种思路,类型断言&pick等等都可以。
原因:定义的返回值类型T,实际的返回值是T的子类型
如果不能修改返回值,就要修改返回值类型
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): User {
return {
id: u.id,
kind: 'customer'
}
}