awesome-typescript icon indicating copy to clipboard operation
awesome-typescript copied to clipboard

「重学TS 2.0 」TS 练习题第一题

Open semlinker opened this issue 4 years ago • 48 comments

第一题

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'
  }
}

以上代码为什么会提示错误,应该如何解决上述问题?

请在下面评论你的答案

semlinker avatar Sep 14 '21 15:09 semlinker

// 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上,如果不用扩展运算符进行返回,并后续覆盖属性的话,函数接收的参数如上述缩写,就会有问题

pipeng119 avatar Sep 15 '21 02:09 pipeng119

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];
};

fengsai avatar Sep 15 '21 03:09 fengsai

为什么报错?

  1. 因为 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'
  }
}

yzycool avatar Sep 16 '21 14:09 yzycool

返回类型 让其自动推导

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'
  };
}

chenxch avatar Sep 18 '21 01:09 chenxch

@semlinker 这个可以解释下吗?不太理解

binvb avatar Sep 18 '21 10:09 binvb

可以直接重新定义返回数据类型 type User = { id: number; kind: string; };

function makeCustomer<T extends User>(u: T): User { return { id: u.id, kind: 'customer' } }

wangxiaoer5200 avatar Sep 18 '21 13:09 wangxiaoer5200

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'
  }
}

PedroGuo avatar Sep 19 '21 12:09 PedroGuo


function makeSuperUser<T extends User>(t: T): T {
    return {
        id: t.id,
        name: 'customer'
    } as T // 添加as 类型断言
}

DevilProgrammer avatar Sep 20 '21 02:09 DevilProgrammer

其实是需要知道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)

Col0ring avatar Sep 20 '21 18:09 Col0ring

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

mingzhans avatar Sep 21 '21 14:09 mingzhans

第一种方案

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'
  }
}

fltenwall avatar Sep 22 '21 06:09 fltenwall

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'
  };
}

解题思路如注释

zhaoxiongfei avatar Oct 01 '21 05:10 zhaoxiongfei

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的情况下不推荐类型断言。

xzp-git avatar Oct 14 '21 07:10 xzp-git

@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。

lemondreamtobe avatar Nov 01 '21 08:11 lemondreamtobe

这其实就是一个类型兼容的问题,举个例子: 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', } } 以后如果想明白更好的写法再来补充

xixisanmao avatar Nov 05 '21 09:11 xixisanmao

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 {};

lwt09 avatar Nov 19 '21 06:11 lwt09

上面用 ... 扩展运算符的,例如传入参数 makeCustomer({ id: 1, kind: 'yes', name: 'qiuxc' }) 会返回 { id: 1, kind: 'yes', name: 'qiuxc' },而题意只想返回 idkind 可以使用 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' }

qiuxchao avatar Nov 29 '21 07:11 qiuxchao

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' } }

a572251465 avatar Nov 30 '21 15:11 a572251465

type User = { id: number; kind: string; };

function makeCustomer<T extends User>(u: T): T { return { ...u, id: u.id, kind: u.kind } }

pdc-cool avatar Dec 09 '21 07:12 pdc-cool

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,这明显是不安全的。

解决思路

修改函数返回

  1. 使用拓展运算符
  2. Object.assign()

修改函数返回类型

Pick<T, 'id' | 'kind'> 等等,上面已有很多例子

wermdany avatar Jan 13 '22 03:01 wermdany

https://juejin.cn/post/7062903623470514207 水了一篇文章,这里面写了48道题目的解法逻辑解析。😁

ihoneys avatar Feb 10 '22 14:02 ihoneys

核心的关键是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。

  1. Pick的方法(见上)
  2. spread operator方法(见上)
  3. as断言方法(见上)
  4. 让TS自己去inference(见上)
  5. any方法(不好)
function makeCustomer<T extends User>(u: T): any {
  return {
    id: u.id,
    kind: 'customer'
  } 
}
  1. 使用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
})

codeyourwayup avatar Mar 27 '22 03:03 codeyourwayup

关于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 });

类似题目: image

解决方案:


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];
};

codeyourwayup avatar Mar 27 '22 04:03 codeyourwayup

/*
  结论:返回值限制为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'
  }
}

zav1n avatar Apr 13 '22 08:04 zav1n

定义的返回值类型是泛型 T 函数返回的类型是 {id: number, kind: string} ,所以不匹配

<T extends User> 代表 泛型 T 包含 User 的属性且可以多于 User的属性

修改为 function makeCustomer<T extends User>(u: T): T { return { ...u, kind: 'customer' } }

Jarryxin avatar May 16 '22 09:05 Jarryxin

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即可

YJCCreateAHistory avatar May 23 '22 01:05 YJCCreateAHistory

type User = {
  id: number;
  kind: string;
};

// 返回T类型
function makeCustomer<T extends User>(u: T):T{
  return {
    ...u,
    id: u.id,
    kind: 'customer'
  }
}

xiliangzhou6216 avatar May 30 '22 07:05 xiliangzhou6216

返回的类型应该与u一致

ghh-l-djl avatar Jun 28 '22 09:06 ghh-l-djl

原题代码

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等等都可以。

lrxugithub avatar Aug 04 '22 06:08 lrxugithub

原因:定义的返回值类型T,实际的返回值是T的子类型

如果不能修改返回值,就要修改返回值类型

type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): User {
  return {
    id: u.id,
    kind: 'customer'
  }
}

wangmeijian avatar Aug 08 '22 12:08 wangmeijian