Reputation: 1337
I've been using React for a while and I'm starting to incorporate Typescript into it. In my previous projects, I was used to creating large "container" objects like this:
const theme = {
colors: {
primary: '#00f',
accent: '#f70',
},
sizes: {
base: '16px',
},
};
How could I define a type for such an object? It seems awkward that I have to define the interface by typing (pardon the pun) every single member of it, then repeat it all over to assign the values:
interface ThemeType { colors: ThemeColors, sizes: ThemeSizes }
interface ThemeColors { primary: string, accent: string }
interface ThemeSizes { base: string }
const theme : ThemeType = {
// ...
};
Is there another way to do it? At least a more compact way?
Upvotes: 0
Views: 519
Reputation: 29007
First of all, you don't need to define each sub-part of an interface if that is not used in your code:
interface ThemeType { colors: ThemeColors, sizes: ThemeSizes }
interface ThemeColors { primary: string, accent: string }
interface ThemeSizes { base: string }
These three interfaces could just be combined into one:
interface ThemeType {
colors: {
primary: string,
accent: string
},
sizes: {
base: string
}
}
Which means that instead of having three interfaces each used once, you now only have a single one.
However, if you truly only ever use the interface once, then you don't necessarily need an interface - TypeScript will implicitly type your variable if you just assign to it:
const theme = {
colors: {
primary: '#00f',
accent: '#f70',
},
sizes: {
base: '16px',
},
};
In this case theme
implicitly has the type
{
colors: {
primary: string,
accent: string
},
sizes: {
base: string
}
}
just like before. However, you don't have to explicitly define it and you can't re-use it. Still, you do maintain type safety - doing theme.colors.primary = 42
will throw a compiler error
Defining a type can be useful. An implicit type here will allow any strings to be used as values which is not strictly correct. For example:
const temp = theme.colors.primary;
theme.colors.accent = temp; //valid assignment and allowed by TS
theme.sizes.base = temp; //invalid assignment yet allowed by TS
You could alleviate that using what's called an "opaque type". This is quite simply a type that "hides" another type. The idea is that two identical opaque types are assignable to one another but two different opaque types aren't, even if the "hidden" type is the same for all. I'm going to use my favourite implementation of TypeScript opaque types from Drew Colthorp which is described in the article Flavoring: Flexible Nominal Typing for TypeScript. Note: the implementation there calls it "Flavouring" which is (as far as I know) a term invented by Mr Colthorp and his team.
interface Flavoring<FlavorT> {
_type?: FlavorT;
}
export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
In short, this is all you need to define opaque types, the usage would be
type ColorHex = Flavour<string, "ColorHexValue">
type SizePx = Flavour<string, "SizePxValue">
let myColor: ColorHex = "#000";
let mySize: SizePx = "42px";
myColor = mySize; //compiler error
You have to pass the inner type (string
) and an arbitrary but unique identifier for it ("ColorHexValue"
and "SizePxValue"
). The secret magic is in the second parameter - since the two don't match, the types are not equivalent. And since it's optional, you can assign values to it but you cannot assign a ColorHex
value to a SizePx
value and vice versa. This allows for very powerful usage of types for very minimal investment:
interface ThemeType {
colors: {
primary: ColorHex,
accent: ColorHex
},
sizes: {
base: SizePx
}
}
const theme: ThemeType = {
colors: {
primary: '#00f',
accent: '#f70',
},
sizes: {
base: '16px',
},
};
const temp = theme.colors.primary;
theme.colors.accent = temp; //valid assignment and allowed by TS
theme.sizes.base = temp; //invalid assignment blocked by TS
It's worth pointing out that ColorHex
won't actually validate that the string contains a valid hex representation. However, it still goes a long way to protecting you from accidentally assigning one string to another in the cases when that doesn't makes sense - assigning a colour to a size.
Upvotes: 1
Reputation: 1212
Interfaces only define the contract for a given object and don't allocate any memory, hence you can't provide initial values.
Not exactly analagous to your example, but you could get strong typing more concisely using classes rather than interfaces as follows:
class ThemeColors { primary: string = '#00f'; accent: string = '#f70'};
class ThemeSizes { base: string = '16px' };
class ThemeType { colors: ThemeColors = new ThemeColors(); sizes: ThemeSizes = new ThemeSizes() }
const theme = new ThemeType()
As per comments below, alternatively you could also do something like this:
class ThemeType {
colors = { primary: '#00f', accent: '#f70' };
sizes = { base: '16px' };
}
const theme = new ThemeType();
Upvotes: 0
Reputation: 51034
Just declare the constant with no type annotation; its type will be inferred as:
{
colors: {
primary: string;
accent: string;
};
sizes: {
base: string;
};
}
If you need to use its type as an annotation elsewhere, you can write:
type ThemeType = typeof theme
Then you can use ThemeType
as an alias for the above type. If you want to refer to the types of the nested objects, they are ThemeType['colors']
and ThemeType['sizes']
.
Upvotes: 2