Generic

Generic — Constraint và Variance

Constraint (where / extends), covariance, contravariance, invariance — hiểu đúng để dùng đúng.

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

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;
}
Câu PV cốt lõi:"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.
Kết hợp lại = type-safe property access tổng quát.

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ậy List<Dog> : List<Animal> không?

3 khả năng:

Covariant
out
Nếu Dog : Animal thì F : F. Subtyping được giữ.
Contravariant
in
Nếu Dog : Animal thì F : F. Đảo chiều.
Invariant
(default)
F và F không có quan hệ subtype.

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>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
Mnemonic: "out → covariant (cùng hướng), in → contravariant (ngược hướng)".Rule of thumb:
  • 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
Function type variance đặc biệt — return covariant, parameter contravariant:
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?

⭐ Câu PV junior+: "Tại sao 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ể Add Cat vào List<Animal> chứa Dog → crash."

Practical use: design API library — thiết kế interface có in/out đúng → caller dùng linh hoạt hơn.

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.