Open20
ジェネリクス: TypeScript
デフォルト型引数
双変 ( Bivariant )
TS はデフォルトで共変かつ反変?
tsconfig.json
で反変にできる
関数の in
と out
で 4 つの変性を指定できる
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}]
bs
を bs2
に代入して 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}]
bs
を as
に代入したら 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())
strictFunctionTypes
が false
だとコンパイルできてしまう
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>
としておくと「T
を get
で使うな」と言われ Box<out T>
としておくと「T
を set
で使うな」と言ってもらえる
結果的にこうするしかなく、より明瞭になる
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
のときだけ出るので、どのみち利用時の誤用はすでに結構つぶされている
TS は Structual Typing なので注意ね
あとはまぁ不変にできるくらいか
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
あまりなんの役に立つかわからんが...
あとはコードの中身を見なくても変性がわかるのでコンパイルが速くなる
測ってないけど