坑爹的面试题
继上次小🐟面试心脏跳动之后,久久没有收到反馈,小🐟很是着急,于是主动向HR问起结果,HR很遗憾的跟她说已经招满了,不过隔壁组有个HC,你要不要试一下,小🐟心有不甘,就跟HR说今天下午可以面试吗?HR拍拍脸,说:安排~

下午三点,面试官准时上线,看到小🐟简历写到精通 Typescript ,拥有多年体操经验,曾获国家体操三等奖, 瞬间来了兴趣,兴奋地忘记流程,拿出珍藏已久的你面试题来考考她,就跟小🐟说:看我文档的这道题,说一下哪个类型不通过,为什么,以及怎么将类型改写可以达到相同的效果。
typescript
interface StudentArrayA<T> {
push(...items: T[]): number;
}
interface StudentArrayB<T> {
push: (...items: T[]) => number;
}
class Person {
getName() {}
}
class Student extends Person {
getScore () {
}
}
declare let StudentArrayA1: StudentArrayA<Student>
declare let StudentArrayA2: StudentArrayA<Person>
StudentArrayA1 = StudentArrayA2
StudentArrayA2 = StudentArrayA1
declare let StudentArrayB1: StudentArrayB<Student>
declare let StudentArrayB2: StudentArrayB<Person>
StudentArrayB1 = StudentArrayB2
StudentArrayB2 = StudentArrayB1
小🐟看到这道题当时的表情是这样的:

内心是这样的:

想了三十分钟之后,跟面试官说:这道题应该没问题吧?面试官很有耐心地打开了ts playground展示了一下,跟小🐟说:回去等消息吧。
ts
interface StudentArrayA<T> {
push(...items: T[]): number;
}
interface StudentArrayB<T> {
push: (...items: T[]) => number;
}
class Persion {
getName() {}
}
class Student extends Persion {
getScore () {
}
}
declare let StudentArrayA1: StudentArrayA<Student>
declare let StudentArrayA2: StudentArrayA<Persion>
StudentArrayA1 = StudentArrayA2
StudentArrayA2 = StudentArrayA1
declare let StudentArrayB1: StudentArrayB<Student>
declare let StudentArrayB2: StudentArrayB<Persion>
StudentArrayB1 = StudentArrayB2
StudentArrayB2 = StudentArrayB1
小🐟很是郁闷,但她秉持着在哪里跌倒就在哪里站起来的原则,开始请教了ChatGPT老师。
两个🌰
我们先看第一个例子:有Animal
和Dog
两个类,其中Dog
继承Animal
,所以Dog≤Animal
的关系。
ts
class Animal {
asPet() {}
}
class Dog extends Animal {
bark() {}
}
// 简单场景
type Getter<T> = () => T;
type Setter<T> = (value: T) => void;
declare let A1: Getter<Animal>
declare let A2: Getter<Dog>
A1 = A2
A2 = A1
declare let B1: Setter<Animal>
declare let B2: Setter<Dog>
B1 = B2
B2 = B1
我们来拆解一下上面的Getter和Setter赋值情况:
- Getter的情况
我们将上面A1、A2改写成类型,根据下面的例子我们可以看到当函数的返回类型是Dog
的时候是可以赋值给返回类型是Animal
,但返回类型Animal
是不能赋值给返回类型是Dog
的。
scss
() => Animal = () => Dog //
() => Dog != () => Animal
- Setter的情况
同样,我们将上面的B1、B2改写成类型,根据下面的例子我们可以看到当函数的参数类型是Animal
的时候是可以赋值给函数参数类型是Dog
,但参数类型是Dog
是不能复制给参数类型是Animal
的。
javascript
(value: Animal) => void != (value: Dog) => void
(value: Dog) => void = (value: Animal) => void
是不是已经找到规律了,那我们下面看一个复杂点的例子,在这里,我们有三个的类,每个类在上一个类的基础上添加了一个独特的方法。我们使用 ≼
符号表达子类型关系,A ≼ B
意为 A 是 B 的子类型,在这里的例子中,易得 Corgi ≼ Dog ≼ Animal
。现在我们有一个新的函数,它接收一个函数作为参数,其类型为 Dog -> Dog
(即参数类型与返回值均为 Dog
)。由于我们的类型是Dog -> Dog
,所以我们需要考虑Dog -> Animal/Corgi
,Animal/Corgi -> Dog
,Corgi/Animal
-> Corgi/Animal
这样的类型,来排列组合一下几种情况:
ts
class Animal {
asPet() {}
}
class Dog extends Animal {
bark() {}
}
class Corgi extends Dog {
cute() {}
}
type DogFactory = (args: Dog) => Dog;
function transformDogAndBark(dogFactory: DogFactory) {
const dog = dogFactory(new Dog());
dog.bark();
}
// 情况1
const CorgiAndCorgi: (args: Corgi) => Corgi = (args) => args
transformDogAndBark(CorgiAndCorgi)
// 情况2
const AnimalAndAnimal: (args: Animal) => Animal = (args) => args
transformDogAndBark(AnimalAndAnimal)
// 情况3
const CorgiAndAnimal: (args: Corgi) => Animal = (args) => new Animal()
transformDogAndBark(CorgiAndAnimal)
// 情况4
const AnimalAndCorgi: (args: Animal) => Corgi = (args) => new Corgi()
transformDogAndBark(AnimalAndCorgi)
// 情况5
const DogAndCorgi: (args: Dog) => Corgi = (args) => new Corgi()
transformDogAndBark(DogAndCorgi)
// 情况6
const CorgiAndDog: (args: Corgi) => Dog = (args) => new Dog()
transformDogAndBark(CorgiAndDog)
// 情况7
const DogAndAnimal: (args: Dog) => Animal = (args) => new Animal()
transformDogAndBark(DogAndAnimal)
// 情况8
const AnimalAndDog: (args: Animal) => Dog = (args) => new Dog()
transformDogAndBark(AnimalAndDog)
Corgi -> Corgi
:我们在transformDogAndBark
中,会传入一只狗,并让返回的狗狗叫两声听听,看起来好像没问题,但是Corgi -> Corgi
函数只能接受柯基,内部可能调用了柯基才有的逻辑,如果我们传了个柴犬,那程序可能就崩溃了。但返回值没问题,因为不管是柯基还是柴犬都能叫嘛。Animal -> Animal
:有了上一点的经验我们一看就知道不行,因为它的返回值可能是任何动物,但不是任何动物都会狗叫。Corgi -> Animal
:第一点是参数类型有问题,第二点是返回值类有问题,这一点则是参数类型和返回值都有问题。Animal -> Corgi
:只剩下这一个正确答案了,如果还不行的话就离谱了。还是先来分析一波,首先我们会传入一只狗,好的,没问题,Animal 有的方法 Dog 都有。接着我们会让返回的物种叫两声,这里返回的是柯基!它可以叫!所以没问题!
归纳一下上面的情况,我们会发现,作为参数的函数,它的入参允许是函数入参类型的父类型(实际入参 Animal
,类型入参Dog
),不允许为子类型(实际入参Corgi
,类型入参Dog
),而它的返回值允许是函数返回类型的子类型(实际返回值Corgi
,类型返回值Dog
),不允许是父类型(实际返回值Animal
,类型返回值 Dog
)。
根据上面两个例子我们可以总结出:如果把(Animal → Corgi)
记为A
,把Dog → Dog
记为B
,则A ≼ B
,也就是A
是B
的子集。
ts
class Animal {
asPet() {}
}
class Dog extends Animal {
bark() {}
}
class Corgi extends Dog {
cute() {}
}
declare let B: (args: Dog) => Dog;
declare let A: (args: Animal) => Corgi;
B = A
验证一下,没问题,那为什么会出现这种情况?
协变与逆变
这个时候我们可以引入协变(covariance )与逆变(contravariance)的概念了。
协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。
---- .Net基础知识
根据上面的两个例子,我们可以发现函数的参数只能接受自己的子类,返回值只能接受自己的父类,借用上面的概念:协变是比原始指定的派生类型的派生程度更大的类型, 逆变是比原始指定的派生类型的派生程度更小的类型, 所以可以得出结论 :函数返回值为协变,函数参数为逆变。
所以我们假设存在 Corgi ≼ Dog
的情况,如果它遵循协变,则有 (T → Corgi) ≼ (T → Dog)
,即 A、B 在被作为函数返回值类型以后仍然遵循一致的子类型关系。而对于参数,由于其遵循逆变,则有 (Dog → T) ≼ (Corgi → T)
,即 A、B 被作为函数参数类型以后其子类型关系发生逆转。
双变(bivariant or 双向协变? )与不变
还是先看一个例子:当我们写下这段代码的时候,并没有什么问题,但是我们尝试运行一下,但是发现报错了,为什么?
ts
class Animal {
asPet() {}
}
class Dog extends Animal {
bark() {}
}
class Cat extends Animal{}
function trainDog(d: Dog) { }
function cloneAnimal(source: Animal, done: (result: Animal) => void): void { }
let c = new Cat();
// Runtime error here occurs because we end up invoking 'trainDog' with a 'Cat'
cloneAnimal(c, trainDog);
ts
declare let A: (args: Animal) => void
declare let B: (args: Dog) => void
由于Dog
是Animal
的子集,即Dog≼Animal
,根据上面的结论,函数的参数是发生逆变,故等式A
是B
的子集, 即A≼B
,根据typescript类型推断,A
的类型是可以赋值给B
,但B
的类型是不可以赋值A
的。那为什么还是能绕过typescript类型检测呢,想要理解这个问题,去思考两个问题:
Dog[]
是Aniaml[]
子类型吗?- 在Typescript中,
Dog[]
应该是Aniaml[]
子类型吗?
先回答第二个问题:
scss
function checkIfAnimalsAreAwake(arr: Animal[]) {
//...
}
let myPets: Dog[] = [new Dog('spot'), new Dog('fido')];
checkIfAnimalsAreAwake(myPets);
看上面的代码,如果答案是不是的话,就应该报错,对吧?但是上面的代码是100%正确的,只要checkIfAnimalsAreAwake
不修改数组,这个程序永远是对的。
再看第二个问题,当类型系统决定Dog[]
是否是Animal[]
的子类型时,它会执行以下计算:
Dog[]
可以分配给Animal[]
吗?Dog[]
的每个成员都可以分配给Animal[]
吗?Dog[].push
是否可分配给Animal[].push
?- 类型
(x: Dog) => number
可分配给(x: Animal) => number
吗?(x: Dog) => number
中的第一个参数类型是否可分配给(x: Animal) => number
中的第一个参数类型?Dog
是否可以分配给Animal
?- 可以分配
- 类型
从这里看到在typescript
类型检测中,类型系统必须询问类型(x: Dog) => number
可分配给(x: Animal) => number
吗?这正好与我们刚开始的问题相同,如果typescript
强制参数发生逆变 (要求Animal
可以分配给Dog
),那么Dog[]
将不能分配给Animal[]
,那正好与我们第二个问题冲突了,所以我们可以得出结论:双变就是在typescript检查函数参数类型时,既可以发生协变,也可以发生逆变。 用公式推导就是当Dog ≼ Animal
可以推导出 Dog -> void ≼ Animal -> void
与 Animal -> void ≼ Dog -> void
。
如何避免
由于函数参数的双变,在写代码的时候难免会出现不注意的情况,所以在typescript 2.6版本的时候新增strictFunctionTypes
,当开启strictFunctionTypes
选项时,会将函数参数类型做强制逆变检查。下面来看一下demo:
ts
class Animal {
asPet(){}
}
class Dog extends Animal {
name(){}
}
class Cat extends Animal {
age(){}
}
declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f1 = f2; // Error with --strictFunctionTypes
f2 = f1; // Ok
f2 = f3; // Error
再举一个例子:
ts
class Animal {
asPet(){}
}
class Dog extends Animal {
name(){}
}
interface Comparer<T> {
compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
我们发现刚刚发现的规则又失效了,第一个赋值仍然是允许的,为什么呢?因为compare
被声明为方法。实际上, T
在 Comparer<T>
中是双变的,因为它只用于方法参数位置。那怎么能修复这个问题呢?
- 将
compare
改写成具有函数类型的属性
ts
class Animal {
asPet(){}
}
class Dog extends Animal {
name(){}
}
interface Comparer<T> {
compare: (a: T, b: T) => number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Error
dogComparer = animalComparer; // Ok
- in/out表明要发生协变还是逆变
ts
class Animal {
asPet(){}
}
class Dog extends Animal {
name(){}
}
interface Comparer<in T> {
compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
了解了双变之后,我们再来看一下不变:
ts
class Animal {
asPet(){}
}
class Dog extends Animal {
wang(){}
}
class Cat extends Animal {
miao() {}
}
declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f2 = f3
f3 = f2
不变的概念就比较简单了,两种类型既不会发生协变,也不会逆变,完全没有关系的两种类型。有一个需要注意的点
当一个T
同时用于输出和输入位置时,它就会变得不变。两个不同的State<T>
不能互换,除非它们的T
相同。换句话说,State<Dog>
和 State<Animal>
不能相互替代。所以我们可以使用in out
表明不变。
csharp
interface State<in out T> {
get: () => T;
set: (value: T) => void;
}
这时候有同学会提问,我就是不写呢,其实在大多是的场景下是不需要的,但还是例外场景:
ts
type Foo<T> = {
x: T;
f: Bar<T>;
}
type Bar<U> = (x: Baz<U[]>) => void;
type Baz<V> = {
value: Foo<V[]>;
}
declare let foo1: Foo<unknown>;
declare let foo2: Foo<string>;
foo1 = foo2; // Should be an error but isn't ❌
foo2 = foo1; // Error - correct ✅
结论
我们现在知道什么是协变,逆变,双变以及不变,那我们再看面试题:
typescript
interface StudentArrayA<T> {
push(...items: T[]): number;
}
interface StudentArrayB<T> {
push: (...items: T[]) => number;
}
class Person {
getName() {}
}
class Student extends Person {
getScore () {
}
}
declare let StudentArrayA1: StudentArrayA<Student>
declare let StudentArrayA2: StudentArrayA<Person>
StudentArrayA1 = StudentArrayA2
StudentArrayA2 = StudentArrayA1
declare let StudentArrayB1: StudentArrayB<Student>
declare let StudentArrayB2: StudentArrayB<Person>
StudentArrayB1 = StudentArrayB2
StudentArrayB2 = StudentArrayB1
我们知道函数作为属性会进行更严格的检查,由于Student是Person的子集(Student ≼ Person
),如果发生协变等式(T → Student) ≼ (T → Person)
成立,如果发生逆变等式(Person → T) ≼ (Student → T)
成立,22/23行代码发生双变,故没问题,28/29发生逆变,必有一个有问题,我们套入公式,由于(Person → T)
是(Student → T)
子集,在typescript是可以将父集赋值给子集,故28行代码没问题,那29行必然有问题。
ts
interface StudentArrayA<T> {
push(...items: T[]): number;
}
interface StudentArrayB<T> {
push: (...items: T[]) => number;
}
class Person {
getName() {}
}
class Student extends Person {
getScore () {
}
}
declare let StudentArrayA1: StudentArrayA<Student>
declare let StudentArrayA2: StudentArrayA<Person>
StudentArrayA1 = StudentArrayA2
StudentArrayA2 = StudentArrayA1
declare let StudentArrayB1: StudentArrayB<Student>
declare let StudentArrayB2: StudentArrayB<Person>
StudentArrayB1 = StudentArrayB2
StudentArrayB2 = StudentArrayB1