Converting a token to XXX

If your goal is to create your own output, you'll have to go through 2 steps:

  • Converting the tokens to the desired output, e.g: CSS, Js, Swift, etc...

  • Formatting the converted tokens into the desired template

We can't know what's the perfect template for your needs, but we can definitely help you converting tokens to your desired output. That's why we created the converters.

What's a converter?

A converter is a function that takes a resolvable alias strategy, an unresolvable alias strategy, and returns a function that takes a TokenState and will convert it to any available format. This is the way to build your custom output with the SDK. We provide you the conversion, and you only have to format it the way you want. Here is an example:

import {
  dimensionToCss,
  breakpointToCss,
  textStyleToCss,
  createResolveAliasStrategy 
} from '@specify/sdk/css'

const strategy = createResolveAliasStrategy()

const output = tokenState.matchByType(
  {
    dimension: dimensionToCss(strategy),
    breakpoint: breakpointToCss(strategy),
    textStyle: textStyleToCss(strategy)
  }, 
  _ => undefined
)

This is as straightforward as it looks, but you're probably wondering what is the strategy const.

The alias strategies

Let's focus on what are the strategies. When working with the SDTF, you are probably aware that a token can contains an alias at a top level, mode level, or value level. When converting a token, we need to decide what we want to do with the aliases. In the case of resolvable aliases, there will be generally 2 strategies:

  • Resolve the alias to retrieve the value and use the value as is

  • If the target language supports it, convert the alias itself to a variable, alias, etc... Although it may sounds simple, the reality is that there's a lot of pitfalls, especially when trying to work with the aliases without resolving them. This is the reason why we provide premade strategies, so you don't have to go through all the pain of implementing it.

Note that there is strategies both for resolvable and unresolvable aliases. Generally speaking, the recommended way to work with a token is the following:

import { dimensionToCss } from '@specify/sdk/css'

if (!tokenState.isFullyResolvable) {
  return
}

const strategy = createResolveAliasStrategy()

tokenState.matchByType(
  {
    dimension: dimensionToCss(strategy)
  }, 
  _ => undefined
)

The most important here are the first lines. If the token is not fully resolvable, we just ignore it, and you should do it this way as much as possible. Working with unresolvable aliases is not really desirable, so it's better to just avoid doing it. That's why the default unresolvable alias strategy is to throw an error, and checking if the token is fully resolvable or not is enough to make sure you'll avoid a lot of problems.

Using a strategy

Let's take CSS as example, and compare the output of various strategies.

Resolve alias strategy

This strategy will resolve the token in order to retrieve the value and convert it to CSS:

const strategy = createResolveAliasStrategy()

// Token value: [{ value: 12, unit: 'px'}, { $alias: 'mySpacing', $mode: 'default' }]
tokenState.matchByType(
  {
    spacings: spacingsToCss(strategy) // -> 12px 24px
  }, 
  _ => undefined
)

Alias to var strategy

Because CSS supports the aliasing through the var(...) notation, we can rely on it to convert our aliases to a CSS variable:

const strategy = createAliasToVarStrategy({ tokenNotInCollectionNameTemplate: '--{{token}}}' })

// Token value: [{ value: 12, unit: 'px'}, { $alias: 'mySpacing', $mode: 'default' }]
tokenState.matchByType(
  {
    spacings: spacingsToCss(strategy) // -> 12px var(--mySpacing)
  }, 
  _ => undefined
)

Throw on unresolvable strategy

This strategy is the default one for the unresolvable aliases. As the name is saying it, It'll throw an error if we encounter an unresolvable alias during the conversion:

const resolvableStrategy = createResolveAliasStrategy()
const unresolvableStrategy = createThrowOnUnresolvableStrategy()

// Token value: [{ value: 12, unit: 'px'}, { $alias: 'wrong.token', $mode: 'default' }]
tokenState.matchByType(
  {
    // -> Error: 'wrong.token' is unresolvable
    spacings: spacingsToCss(resolvableStrategy, unresolvableStrategy)
  }, 
  _ => undefined
)

Note that this strategy is the default one, so you don't need to pass anything.

Ignore unresolvable strategy

This strategy is ignoring all the unresolvable aliases if it finds one, which mean that it'll return undefined instead.

const resolvableStrategy = createResolveAliasStrategy()
const unresolvableStrategy = createIgnoreUnresolvableStrategy()

// Token value: [{ value: 12, unit: 'px'}, { $alias: 'wrong.token', $mode: 'default' }]
tokenState.matchByType(
  {
    // -> 12px undefined
    spacings: spacingsToCss(resolvableStrategy, unresolvableStrategy)
  }, 
  _ => undefined
)

Unresolvable alias to variable

This strategy is actually quite special. It'll convert unresolvable aliases to a CSS variable. Even if an alias is unresolvable, we still have a lot of informations on the target based on the path, mode, and the token referencing the alias.

const resolvableStrategy = createResolveAliasStrategy()
const unresolvableStrategy = createUnresolvableAliasToVarStrategy({
  tokenNotInCollectionNameTemplate: '--{{token}}'
})

// Token value: [{ value: 12, unit: 'px'}, { $alias: 'wrong.token', $mode: 'default' }]
tokenState.matchByType(
  {
    // -> 12px var(--wrong-token)
    spacings: spacingsToCss(resolvableStrategy, unresolvableStrategy)
  }, 
  _ => undefined
)

Note that top level aliases lacks to much informations, so if this strategy encounter one, it'll throw an error.

Create your own strategy

If the provided strategies don't satisfy you, you can still decide to create your own strategy! In the case of a resolvable alias strategy, you'll have to create a function that follows this type:

type ResolvableAliasStrategy<
  Return, 
  Composites extends SpecifyDesignTokenTypeName
> = <
  Alias extends AllResolvableAlias = AllResolvableAlias,
>(
  alias: Alias,
) => Alias extends ResolvableTopLevelAlias
  ? {
      [mode: string]: Alias extends AllResolvableAlias<Composites>
        ? Record<string, Return>
        : Return;
    }
  : Alias extends AllResolvableAlias<Composites>
    ? Record<string, Return>
    : Return;

Let's breakdown the signature:

  • Return is the desired return type for your strategy. Generally it'll be the same than the converters

  • Composites is a union of the types of the composites tokens. Those tokens are expected to be converted to a Record<string, Return> as they have multiple outputs

  • About the Alias, it's a generic that will be one of these:

type AllResolvableAlias<
  Type extends SpecifyDesignTokenTypeName = SpecifyDesignTokenTypeName,
> =
  | ResolvableValueLevelAlias<Type>
  | ResolvableModeLevelAlias<Type>
  | ResolvableTopLevelAlias<Type>;

Finally, you can notice that in the case of a top level alias, you need to return an object containing the modes, and then the return value.

Here is an example of the CSS resolvable alias strategy:

type CssResolvableAliasStrategy = ResolvableAliasStrategy<
  string,
  'font' | 'textStyle' | 'transition'
>;

We expect the return type to be a string, and the tokens font, textStyle and transition to return multiple values, thus, a Record<string, string>.

In general, you'll probably don't want to work with ResolvableAliasStrategy, but rather with the converter strategy, e.g: CssResolvableAliasStrategy. Here is an example of an implementation:

import {
  ResolvableValueLevelAlias,
  ResolvableModeLevelAlias,
  ResolvableTopLevelAlias
} from '@specify/specify-design-token-format'
import { CssResolvableAliasStrategy } from '@specifyapp/sdk/css'

const myCustomStrategy: CssResolvableAliasStrategy = (alias) => {
  if (alias instanceof ResolvableTopLevelAlias) {
    ...
  } else if (alias instanceof ResolvableModeLevelAlias) {
    ...
  } else {
    ...
  }
}

tokenState.matchByType({
  dimension: dimensionToCss(myCustomStrategy)
}, _ => undefined)

The output of a converter

Primitives tokens

Primitives tokens are generally quite straightforward to convert to a different output. So almost every time, the output of a primitive token will be a single output. Here is an example with a dimension token converted to CSS:

Input:

{
    value: 12,
    unit: "px"
}

Output:

"12px"

Composites tokens

Composites tokens are a bit more complex than the simples ones. The main issue is that they contain a lot of informations, and most of the time we need multiple outputs to convert all the data. To do so, a converted composites token will output an object containing all the outputs. Here is an example with a transition token that we convert to a CSS output.

Input:

{
  duration: {
    value: 12,
    unit: 's',
  },
  delay: {
    value: 0,
    unit: 'ms',
  },
  timingFunction: [0.1, 0.2, 0.1, 0.4]
}

Output:

{
  delay: "12s",
  duration: "0ms",
  "timing-function": "cubic-bezier(0.1, 0.2, 0.1, 0.4)"
}

In the case of a transition, we can't assume on which property the transition will be applied, so we have to split everything in different tokens so you can build your transition on the property that you want.

If you want more informations on which tokens are primitives and which ones are composites, and on the inputs and outputs, you can have a look to the reference of each converters:

Last updated