The Essentials of ReadT and WriteT Generics in Angular Signal Input Functions
Signal inputs, a notable feature introduced in version 17.1.0, have brought significant advancements in our coding practices. While I’ve previously delved into this feature extensively, there seems to be lingering confusion around the generics used with this function. This article aims to demystify the usage of ReadT
and WriteT
generics:
@Component({ })
export class UserProfileComponent {
👇 👇
skills = input<ReadT, WriteT>()
}
Understanding the Role of ReadT
Generic
The ReadT
generic plays a crucial role in inferring the signal type value during read operations within a component. For instance:
@Component({ })
export class UserProfileComponent {
// `ReadT` inferred as `string` from the initial value
id = input('');
// Explicitly setting `ReadT` as (string | number)
foo = input<string | number>(1)
constructor() {
effect(() => {
// `this.id()` type is string
console.log(this.id());
// `this.foo()` type is (string | number)
console.log(this.foo());
});
}
}
In scenarios where an initial value isn’t provided, and no explicit generic is passed, the type defaults to unknown
. Conversely, if a generic is defined without an initial value, an undefined
type is automatically included:
@Component({ })
export class UserProfileComponent {
// `ReadT` inferred as `unknown` because we didn't provide initial value
id = input();
// Explicilty sets ReadT as (string | number) with no initial value
foo = input<string | number>();
// `ReadT` inferred as `unknown`
bar = input.required();
// `ReadT` inferred as `string`
baz = input.required<string>();
constructor() {
effect(() => {
// `this.id()` type is `unknown`
console.log(this.id());
// `this.foo()` type is (string | number | undefined)
console.log(this.foo());
// `this.bar()` type is `unknown`
console.log(this.bar());
// `this.baz()` type is string
console.log(this.baz());
});
}
}
Exploring the WriteT
Generic
The WriteT
generic is primarily utilized when working with the transform
option. It defines the types of values that can be written to the input. The transform
function receives a WriteT
type and is expected to return a ReadT
type value.
Consider the following example:
@Component({ })
export class UserProfileComponent {
skills = input<string[], string | string[]>([], {
// `value` can be either `string` or `string[]`
transform(value) {
// Should return `string[]`
return Array.isArray(value) ? value : [value];
}
});
}
Here, our signal type is string[]
, but we can pass to the input both string
and string[]
. The type of the value passed to our transform function is string | string[]
, and it returns the ReadT
type, which in this case is string[]
.
If you set only the ReadT
generic without defining WriteT
, you will encounter limitations:
@Component({ })
export class UserProfileComponent {
skills = input<string[]>([], {
// Results in a compilation error
transform(value) {
return Array.isArray(value) ? value : [value];
}
});
}
In this scenario, attempting to use the transform
function will lead to a compilation error because WriteT
is undefined
.
Furthermore, when no generics are explicitly passed, the WriteT
defaults to unknown
:
@Component({ })
export class UserProfileComponent {
// `ReadT` = `number`, `WriteT` = `unknown`
foo = input(1, {
// `value` is `unknown`
transform(value) {
// Must return a number
return 2;
},
});
}
In cases where you use the required
option without passing any generic, ReadT
will be inferred from the return type of the transform
function, and WriteT
will be unknown
:
@Component({ })
export class UserProfileComponent {
// `ReadT` = `number`, `WriteT` = `unknown`
foo = input.required({
// `value` is `unknown`
transform(value) {
return 1;
},
});
}
Always specify both generics when using the transform
function. This ensures clarity and avoids type-related errors.
Follow me on Medium or Twitter to read more about Angular and JS!