You create an object that matches the interface, and pass that into useState
, like this:
const [state, setState] = useState({name: "John", age: 30});
You can also be explicit about the type of that state variable, because useState
is generic:
const [state, setState] = useState<Person>({name: "John", age: 30});
but you don't have to be. TypeScript's type checking is structural, not nominal,¹ meaning that any object with appropriately-matching properties is a match for the state.
If you may not have a person, allow null
or undefined
:
const [state, setState] = useState<Person | null>(null);
// or
const [state, setState] = useState<Person | undefined>(undefined);
In that case, since the type would be null
or undefined
if it were just inferred from what you pass into useState
, you need the generic type parameter on the call.
¹ For me, this concept was foundational in TypeScript. It's not so much that something is a type as it is in (say) Java, it's that something matches a type. This is perfectly valid TypeScript:
interface A {
name: string;
age: number;
}
interface B {
name: string;
age: number;
}
let a: A = {name: "Joe", age: 27};
let b: B;
b = a;
It doesn't matter that b
is declared as type B
and a
is declared as type A
, you can do b = a;
because a
's type is structurally compatible with b
's type (in this case, they're identical).
This is also perfectly valid:
interface A {
name: string;
age: number;
rank: string;
}
interface B {
name: string;
age: number;
}
let a: A = {name: "Joe", age: 27, rank: "Junior Petty Officer"};
let b: B;
b = a;
It's okay that a
's type has a property (rank
) that b
's type doesn't have. It's still compatible with b
's type.