Generic — Constraint và Variance
1. Generic constraint
Constraint hạn chế type parameter T phải thoả điều kiện gì.
Constraint C#
// T là reference type
public class Cache<T> where T : class { }
// T là value type (struct)
public class Counter<T> where T : struct { }
// T là non-nullable value type
public class CounterNonNull<T> where T : struct, System.Enum { }
// T có constructor không tham số
public class Factory<T> where T : new() {
public T Create() => new T();
}
// Combine
public class Repository<T> where T : class, IEntity, new() { }
// T implement IComparable<T>
public static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
// T kế thừa Animal
public class AnimalCage<T> where T : Animal { }
// T là static interface (C# 11+) — operator
public static T Sum<T>(IEnumerable<T> nums) where T : INumber<T>
=> nums.Aggregate(T.Zero, (a, b) => a + b);
// T phải là U hoặc subtype của U
public class Container<T, U> where T : U { }
public class Notifier<T> where T : Animal {
public void Notify<U>(U pet) where U : T { ... }
}
Constraint TypeScript
// T có property length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
// T là subtype của Animal
function feed<T extends Animal>(a: T) { a.eat(); }
// keyof constraint — pattern PV hot
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice" };
get(user, "name"); // string
get(user, "age"); // ❌ "age" không phải key
// Default + constraint
interface Config<T extends object = {}> {
data: T;
}
keyof T là gì và T[K] là gì?"keyof T= union các key của T dưới dạng string literal type.keyof User="id" | "name".T[K](indexed access) = type của value ứng với key K.User["name"]=string.
2. Pattern hay gặp với constraint
Pattern 1 — Repository generic
public interface IEntity {
int Id { get; set; }
}
public abstract class Repository<T> where T : class, IEntity, new() {
protected readonly DbContext _ctx;
public Repository(DbContext ctx) => _ctx = ctx;
public Task<T?> GetByIdAsync(int id) =>
_ctx.Set<T>().FindAsync(id).AsTask();
public async Task AddAsync(T entity) {
_ctx.Set<T>().Add(entity);
await _ctx.SaveChangesAsync();
}
}
Pattern 2 — Type-safe builder
class QueryBuilder<T = {}> {
constructor(private state: T = {} as T) {}
where<K extends string, V>(key: K, value: V): QueryBuilder<T & Record<K, V>> {
return new QueryBuilder({ ...this.state, [key]: value } as T & Record<K, V>);
}
build(): T { return this.state; }
}
const q = new QueryBuilder()
.where("name", "Alice")
.where("age", 25)
.build();
// q: { name: string; age: number }
Pattern 3 — Constraint + default + factory
public class Factory<T> where T : new() {
public T Create() => new T();
public List<T> CreateMany(int n) => Enumerable.Range(0, n).Select(_ => new T()).ToList();
}
3. Variance — bài học sâu
Variance: nếu
Dog : Animal, vậyList<Dog> : List<Animal>không?
3 khả năng:
Vì sao quan trọng?
// Liệu code này safe?
List<Dog> dogs = new() { new Dog("Rex") };
List<Animal> animals = dogs; // covariant?
animals.Add(new Cat("Tom")); // ⚠️ Cat vào list of Dog!
Dog d = dogs[1]; // CRASH
→ List<T> là invariant (T xuất hiện cả input và output) — đúng đắn.
Covariant — out parameter
// IEnumerable<T> là COVARIANT — T chỉ ở vị trí output (return)
public interface IEnumerable<out T> {
IEnumerator<T> GetEnumerator(); // T ở output
}
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // ✅ OK — chỉ đọc, không add Cat được
Contravariant — in parameter
// IComparer<T> là CONTRAVARIANT — T chỉ ở input
public interface IComparer<in T> {
int Compare(T x, T y);
}
IComparer<Animal> animalCmp = new AnimalAgeComparer();
IComparer<Dog> dogCmp = animalCmp; // ✅ — so sánh được Dog vì so được Animal
- Producer (output) → covariant.
- Consumer (input) → contravariant.
- Mixed (cả input và output) → invariant.
Variance trong TS
TS variance automatically inferred từ cách type parameter được dùng:
// Covariant — T chỉ ở return
interface Producer<T> {
produce(): T;
}
declare const dogProd: Producer<Dog>;
const animalProd: Producer<Animal> = dogProd; // ✅ covariant
// Contravariant — T chỉ ở input
interface Consumer<T> {
consume(value: T): void;
}
declare const animalCon: Consumer<Animal>;
const dogCon: Consumer<Dog> = animalCon; // ✅ contravariant
TypeScript 4.7+ thêm explicit variance annotation (in, out):
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(x: T): void; }
interface Pair<in out T> { value: T; setValue(x: T): void; } // invariant
type Fn1 = (a: Animal) => Dog;
type Fn2 = (a: Dog) => Animal;
// Fn1 IS-A Fn2?
// - Return Dog : Animal ✅ (covariant return)
// - Param Animal : Dog? — đảo chiều ✅ (contravariant param)
// → YES, Fn1 assignable to Fn2.
4. Bivariant function parameter — TS quirk
TS mặc định bivariant function parameter (vừa covariant vừa contravariant). Không strict theo lý thuyết. Có thể bật strict:
{ "compilerOptions": { "strictFunctionTypes": true } }
Với strict, function param chỉ contravariant. Hợp lý hơn nhưng có thể phá code cũ.
5. Variance khi nào quan trọng?
IEnumerable<T> covariant mà List<T> không?""
IEnumerable<T>chỉ có method trả T (output) → safe để covariant.List<T>có cảAdd(T)(input) vàthis[int](output) → mixed → invariant. Nếu covariant, có thể AddCatvàoList<Animal>chứaDog→ crash."
Practical use: design API library — thiết kế interface có in/out đúng → caller dùng linh hoạt hơn.