skip to Main Content

I’m so confused, when I assign a superset type to their subset type in the useState hook, why would Typescript not complain about mismatched type? And if it’s intended, how should I type the useState hook to make it complain properly?
`

interface Animal {
    name: string;
    food: string;
    legs: number;
}

interface Dog {
    name: string;
    food: string;
}

const animal: Animal = {
    name: 'animal',
    food: 'animal food',
    legs: 4,
};

function App() {
    const [data, setData] = useState<Dog>(animal);
    ...
}

2

Answers


  1. A static type checker can use either the name (nominal typing) or the structure (structural typing) of types when comparing them against other types (like when checking if one is a subtype of another).

    TypeScript uses a structure typing system.

    See Type Compatibility.

    Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing.

    The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x

    V.S. Nominal typing

    Nominal typing means that two variables are type-compatible if and only if their declarations name the same type. For example, in C, two struct types with different names in the same translation unit are never considered compatible, even if they have identical field declarations.

    Languages like C++, Java, and Swift have primarily nominal type systems.

    // Pseudo code: nominal system
    class Foo { method(input: string) { /* ... */ } }
    class Bar { method(input: string) { /* ... */ } }
    
    let foo: Foo = new Bar(); // Error!
    

    Languages like Go, TypeScript and Elm have primarily structural type systems

    // Pseudo code: structural system
    class Foo { method(input: string) { /* ... */ } }
    class Bar { method(input: string) { /* ... */ } }
    
    let foo: Foo = new Bar(); // Works!
    

    One way to declare nominal typing in TypeScript:

    interface Animal {
        name: string;
        food: string;
        legs: number;
    }
    
    interface Dog {
        name: string;
        food: string;
    }
    
    type Brand<T, K> = T & { __brand: K }
    
    type AnimalNominal = Brand<Animal, 'animal'>;
    type DogNominal = Brand<Dog, 'dog'>
    
    const animal: AnimalNominal = {
        name: 'animal',
        food: 'animal food',
        legs: 4,
        __brand: 'animal'
    };
    
    const dog: DogNominal = {
        name: 'hot dog',
        food: 'dog food',
        __brand: 'dog'
    }
    
    const s1: AnimalNominal = animal // ok
    const s2: AnimalNominal = dog; // error
    
    const s3: DogNominal = animal; // error
    const s4: DogNominal = dog; // ok
    

    Playground Link

    Nominal typing proposal: https://github.com/Microsoft/Typescript/issues/202

    The Brand<T, U> type of utility-types package.

    Define nominal type of U based on type of T

    Login or Signup to reply.
  2. the assignment to be valid due to type compatibility rules.

    TypeScript allows assigning a superset type to a subset type because the superset type contains all the properties and values required by the subset type.

    If you want TypeScript to properly complain about the mismatched type, you can explicitly type the useState() hook with the desired type. In this case, you would want to specify Animal as the type for the useState() hook, as it matches the animal object you are initializing it with.

       const [data, setData] = useState<Animal>(animal);
    

    Remember that TypeScript type checks are performed statically at compile time,

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search