Assuming your string only contains printable ASCII characters, here's a quite clumsy implementation. The last example shows that braced groups are treated as single items.
\catcode`@=11
\def\extract{\futurelet\next\extract@save}
\def\extract@save{%
\ifx\next[%
\expandafter\extract@save@opt
\else
\let\extract@return\@firstofone
\expandafter\extract@
\fi
}
\def\extract@save@opt[#1]{%
\def\extract@return##1{\def#1{##1}}%
\extract@
}
\def\extract@#1#2{%
\edef\extract@string{#1}%
\extract@loop=\z@
\extract@max=#2\relax
\expandafter\extract@i\extract@string\extract
}
\def\extract@i{%
\advance\extract@loop\@ne
\futurelet\next\extract@ii
}
\def\extract@ii{%
\ifx\next\extract
\expandafter\@gobble
\else
\expandafter\extract@iii
\fi}
\def\extract@iii{%
\ifx\next\space@token
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\extract@check@space}%
{\extract@check}%
}
\def\extract@check@space{%
\ifnum\extract@loop=\extract@max
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\extract@return{ }\extract@finish}%
{\expandafter\extract@i\@firstofone}%
}
\def\extract@check#1{%
\ifnum\extract@loop=\extract@max
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\extract@return{#1}\extract@finish}%
{\extract@i}%
}
\def\extract@finish#1\extract{}
\newcount\extract@loop
\newcount\extract@max
\long\def\@firstoftwo#1#2{#1}
\long\def\@secondoftwo#1#2{#2}
\long\def\@firstofone#1{#1}
\long\def\@gobble#1{}
\begingroup\def\\ \\{\endgroup\let\space@token= }\\ \\
\catcode`@=12
X\extract{abc def}{3}X (should be c)
X\extract{abc def}{4}X (should be space)
X\extract{abc def}{5}X (should be d)
X\extract{abc def}{8}X (should be empty)
X\extract{abc }{4}X (should be space)
\def\mystring{abc def}
X\extract\mystring{7}X (should be f)
\extract[\foo]\mystring{6}
{\tt\meaning\foo}
\extract[\foo]{A{BC}D}{2}
{\tt\meaning\foo}
\bye
Without the optional argument, the item is just returned in the input stream; with the optional argument it is saved in the control sequence given between brackets.

Of course I'd do it quite differently. With this code, \extract
is fully expandable as shown with the final \edef
example. Note that you can also count backwards. The code requires an e-TeX engine, so not Knuth TeX.
\input expl3-generic
\ExplSyntaxOn
\cs_new:Npn \extract #1 #2
{
\str_item:fn { #1 } { #2 }
}
\cs_generate_variant:Nn \str_item:nn {f}
\ExplSyntaxOff
X\extract{abc def}{3}X (should be c)
X\extract{abc def}{4}X (should be space)
X\extract{abc def}{5}X (should be d)
X\extract{abc def}{8}X (should be empty)
X\extract{abc }{4}X (should be space)
\def\mystring{abc def}
X\extract\mystring{7}X (should be f)
X\extract\mystring{-2}X (should be e)
\edef\foo{\extract\mystring{6}}
{\tt\meaning\foo}
\bye

foo\bar
which has four tokens but seven detokenized characters.)