Typescript gives us a few tools that we can use to reference existing types, which is very useful if we want to be consistent across our codebase.
Let's see a few examples:
Attention: contrived examples ahead.
Indexed type access
Say we have an interface like this one:
export interface User {
username: string;
age: number;
fullName: {
firstname: string;
lastname: string;
}
}
We can reference all those types inside User
by using index syntax:
let userAge: User['age']; // number
let fullName: User['fullName']; // { firstname: string; lastname: string; }
let lastname: User['fullName']['lastname']; // string
This also works for function signatures, take a look at the fullName
parameter:
function setFullName (user: User, fullName: User['fullName']): User {
return {
...user,
fullName
};
};
Class methods can also be targetted with index syntax, the whole signature or just the return type, which is useful when some method uses a complex return type that doesn't reference any exported type:
class MyClass {
myMethod() {
return {
foo: 'bar'
}
}
}
let myMethod: MyClass['myMethod'] // () => {foo: string;}
let myReturn: ReturnType<MyClass['myMethod']> // {foo: string;}
Indexed type access + instances
We can also apply the same techniques we've seen so far within the realm of instances thanks to the typeof
operator:
const user = {
username: 'picklerick',
age: 70,
fullName: {
firstname: 'Rick',
lastname: 'Sanchez'
}
}
let fullName: typeof user['fullName'] // { firstname: string; lastname: string; }
Last but not least, typeof
with class instances and function signatures:
class MyClass {
prop = 'bar';
method() {
return {
foo: this.prop
}
}
}
const myClass = new MyClass();
let myProp: typeof myClass['prop'] // string
let myMethod: typeof myClass['method'] // () => {foo: string;}
let myReturn: ReturnType<typeof myClass['method']> // {foo: string;}
Indexed type access + Generics
Generic types are inferred, so everything works the same:
interface Action<T> {
type: string;
payload: T;
}
interface User{
id: string;
name: string;
}
let addUserAction: Action<User>; // {type:string; payload: {id:string; name:string}}
let payload : typeof addUserAction['payload'] // User
Indexed type access + tuples
Tuples have similar syntax, but indexes are numbers instead of strings:
type MyTuple = [number, string];
let fst: MyTuple[0] // number
let snd: MyTuple[1] // string
let nope: MyTuple[3] // Error: Tuple type 'MyTuple' of length '2' has no element at index '3'.
We can also use the typeof
operator here:
const myTupleValue: MyTuple = [3, 'hello'];
let fst: typeof myTupleValue[0] // number
let snd: typeof myTupleValue[1] // string
But we have to be explicit with the tuple type otherwise, what used to be a tuple becomes an array and the outcome is very different:
const myTupleValue = [5, 'hello'];
let fst: typeof myTupleValue[0] // string | number
let snd: typeof myTupleValue[1] // string | number
let yep: typeof myTupleValue[3] // string | number
Working with keys
Type keys are also accessible thanks to the keyof
operator:
interface User {
username: string;
age: number;
}
let userKeys: keyof User // "username" | "age"
And, as you may have guessed, our friend typeof
helps us with instances again:
const user = {
username: 'picklerick',
age: 70
}
let userKeys: keyof typeof user // "username" | "age"
Wrapping up
Creating new types with Typescript is super easy and cheap thanks to its structural typing nature, but that doesn't mean we should be careless about that. Having these techniques in our toolbelt can help us keep consistency across our codebase.
Top comments (0)