Open20

ジェネリクス: TypeScript

ほげさんほげさん
class A {}

class B extends A {}

class C extends B {}

共変であり

const bs: B[] = [new B(), new B()]
const as: A[] = bs

console.log(as) // [B: {}, B: {}]

反変でもある

const bs: B[] = [new B(), new B()]
const cs: C[] = bs

console.log(cs) // [B: {}, B: {}]
ほげさんほげさん

こっちの方がいいか

class A {}

class B extends A {
  value: number
  
  constructor(value: number) {
    super()
    this.value = value
  }
}

class C extends B {
  value: number

  constructor(value: number) {
    super(value)
    this.value = value
  }
}

const bs: B[] = [new B(1), new B(2)]
const as: A[] = bs

console.log(as) // [B: {value: 1}, B: {value: 2}]
ほげさんほげさん

bsbs2 に代入して bs2 を操作したとき、bs も変わる

const bs: B[] = [new B(1), new B(2)]
const bs2 = bs
bs2[0] = new B(3)

console.log(bs) // [B: {value: 3}, B: {value: 2}]
ほげさんほげさん

bsas に代入したら C が足せるが、B[]C が入ることになってしまう

class A {}

class B extends A {
  bNumber: number

  constructor(bNumber: number) {
    super()
    this.bNumber = bNumber
  }
}

class C extends B {
  cNumber: number

  constructor(bNumber: number, cNumber: number) {
    super(bNumber)
    this.cNumber = cNumber
  }
}

const bs: B[] = [new B(1), new B(2)]
const as: A[] = bs
as[0] = new C(3, 9)

console.log(bs) // [C: {bNumber: 3, cNumber: 9}, B: {value: 2}]

動くし B として取り出せるが、C である

const b: B = bs[0]
console.log(b) // C: {bNumber: 3, cNumber: 9}
ほげさんほげさん

B として C が取れるならいいが、B として X が取れると問題

const bs: B[] = [new B(1), new B(2)]
const as: A[] = bs
as[0] = new X(5)

console.log(bs) // [X: {xNumber: 5}, B: {value: 2}]

const b: B = bs[0]
console.log(b) // X: {xNumber: 5}
console.log(b.bNumber) // undefined
ほげさんほげさん

戻り値は共変で引数は反変と推論されるっぽい

これはコンパイルも実行も可能

type Getter<T> = () => T

const getter1: Getter<B> = () => new B()
const getter2: Getter<A> = getter1

const a: A = getter2()

console.log(a)

これはだめ

type Setter<T> = (value: T) => void

const setter1: Setter<B> = (value: B) => console.log(value)
const setter2: Setter<A> = setter1

setter2(new A())

これは可能

type Setter<T> = (value: T) => void

const setter1: Setter<B> = (value: B) => console.log(value)
const setter2: Setter<C> = setter1

setter2(new C()) // C
ほげさんほげさん

こういうのはコンパイルエラーになってくれる

type Getter<in T> = () => T
type Setter<out T> = (value: T) => void

明示的に書けるよ、というところなのだろうか

ほげさんほげさん
type BivariantFunction<I, O> = (arg: I) => O;
type CovariantFunction<I, out O> = BivariantFunction<I, O>;
type ContravariantFunction<in I, O> = BivariantFunction<I, O>;
type InvariantFunction<in out I, in out O> = BivariantFunction<I, O>;

declare const biF: BivariantFunction<B, B>;
declare const coF: CovariantFunction<B, B>;
declare const contraF: ContravariantFunction<B, B>;
declare const inF: InvariantFunction<B, B>;

const f01: BivariantFunction<A, B> = biF // A -> B はできてしまう
const f02: BivariantFunction<C, B> = biF
const f03: BivariantFunction<B, A> = biF
const f04: BivariantFunction<B, C> = biF // B -> C ができない

const f05: CovariantFunction<B, A> = coF
const f06: CovariantFunction<B, C> = coF // B -> C ができない

const f07: ContravariantFunction<A, B> = contraF // A -> B ができない
const f08: ContravariantFunction<C, B> = contraF
ほげさんほげさん

双変だからか、こういうことが起こってしまう

class A {
}

class B extends A {
}

class C extends B {
}

class X extends A {
}

class Holder<T> {
  private value: T

  constructor(value: T) {
    this.value = value
  }

  get(): T {
    return this.value
  }
  
  set(value: T) {
    this.value = value
  }
}

const h1:Holder<B> = new Holder<B>(new B())
const h2: Holder<A> = h1

h2.set(new X())

const a: A = h2.get()
console.log(a) // X

const b: B = h1.get()
console.log(b) // X
ほげさんほげさん
class A {}
class B extends A {
  b(): void {
    console.log('I am b')
  }
}
class C extends B {
  c(): void {
    console.log('I am c')
  }
}

interface Box<T> {
  get: () => T;
  set: (value: T) => void;
}

const box1: Box<B> = {
  get: () => new B(),
  set: (value: B) => {
    value.b()
  }
};

const box2: Box<A> = box1

box2.set(new A())

strictFunctionTypesfalse だとコンパイルできてしまう

JS にしたら所詮こうだから

"use strict";
class A {
}
class B extends A {
    b() {
        console.log('I am b');
    }
}
class C extends B {
    c() {
        console.log('I am c');
    }
}
const box1 = {
    get: () => new B(),
    set: (value) => {
        value.b();
    }
};
const box2 = box1;
box2.set(new A());
ほげさんほげさん

これがあるとき

class A {
}

class B extends A {
  b(): void {
    console.log('I am b')
  }
}

class C extends B {
  c(): void {
    console.log('I am c')
  }
}

interface Box<T> {
  get: () => T
  set: (value: T) => void
}

const box1: Box<B> = {
  get: () => new B(),
  set: (value: B) => {
    value.b()
  }
}

strictFunctionTypes: false だと

const box2: Box<A> = box1
// const box3: Box<C> = box1 // コンパイルエラー「get がおかしくなるよ」

box2.set(new A()) // 実行してエラー

true だと

// const box2: Box<A> = box1 // コンパイルエラー「set がおかしくなるよ」
// const box3: Box<C> = box1 // コンパイルエラー「get がおかしくなるよ」
ほげさんほげさん

Box の定義を作るときに間違いに気づけるのがメリット

interface Box<T> {
  get: () => T
  set: (value: T) => void
}

ではなく Box<in T> としておくと「Tget で使うな」と言われ Box<out T> としておくと「Tset で使うな」と言ってもらえる

結果的にこうするしかなく、より明瞭になる

interface Box<in T, out R> {
  get: () => R
  set: (value: T) => void
}

const box1: Box<B, C> = {
  get: () => new C(),
  set: (value: B) => {
    value.b()
  }
}

const box2: Box<B, B> = box1
const box3: Box<C, B> = box1

box2.set(new B()) // I am b
box3.set(new C()) // I am b

ただしこのエラーは strictFunctionTypes のときだけ出るので、どのみち利用時の誤用はすでに結構つぶされている

ほげさんほげさん

あとはまぁ不変にできるくらいか

interface Box<in out T, in out R> {
  get: () => R
  set: (value: T) => void
}

const box1: Box<B, C> = {
  get: () => new C(),
  set: (value: B) => {
    value.b()
  }
}

const box2: Box<B, B> = box1 // エラー
const box3: Box<C, B> = box1 // エラー
const box4: Box<B, C> = box1

あまりなんの役に立つかわからんが...

ほげさんほげさん

あとはコードの中身を見なくても変性がわかるのでコンパイルが速くなる
測ってないけど